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 {PersistentStore} from 'common/persistent_store';
36import {UrlUtils} from 'common/url_utils';
37import {Analytics} from 'logging/analytics';
38import {TRACE_INFO} from 'trace/trace_info';
39import {TraceType} from 'trace/trace_type';
40import {DisplayIdentifier} from 'viewers/common/display_identifier';
41import {UserOptions} from 'viewers/common/user_options';
42import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
43import {UiRect} from 'viewers/components/rects/types2d';
44import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles';
45import {multlineTooltip} from 'viewers/components/styles/tooltip.styles';
46import {viewerCardInnerStyle} from 'viewers/components/styles/viewer_card.styles';
47import {Canvas} from './canvas';
48import {Mapper3D} from './mapper3d';
49import {Distance2D, ShadingMode} from './types3d';
50
51@Component({
52  selector: 'rects-view',
53  template: `
54    <div class="view-header">
55      <div class="title-section">
56        <collapsible-section-title
57          [title]="title"
58          (collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
59        <div class="right-btn-container">
60          <button
61            color="accent"
62            class="shading-mode"
63            (mouseenter)="onInteractionStart([shadingModeButton])"
64            (mouseleave)="onInteractionEnd([shadingModeButton])"
65            mat-icon-button
66            [matTooltip]="getShadingMode()"
67            [disabled]="shadingModes.length < 2"
68            (click)="onShadingModeButtonClicked()" #shadingModeButton>
69            <mat-icon *ngIf="mapper3d.isWireFrame()" class="material-symbols-outlined" aria-hidden="true"> deployed_code </mat-icon>
70            <mat-icon *ngIf="mapper3d.isShadedByGradient()" svgIcon="cube_partial_shade"></mat-icon>
71            <mat-icon *ngIf="mapper3d.isShadedByOpacity()" svgIcon="cube_full_shade"></mat-icon>
72          </button>
73
74          <div class="icon-divider"></div>
75
76          <div class="slider-container">
77            <mat-icon
78              color="accent"
79              matTooltip="Rotation"
80              class="slider-icon"
81              (mouseenter)="onInteractionStart([rotationSlider, rotationSliderIcon])"
82              (mouseleave)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSliderIcon> rotate_90_degrees_ccw </mat-icon>
83            <mat-slider
84              class="slider-rotation"
85              step="0.02"
86              min="0"
87              max="1"
88              aria-label="units"
89              [value]="mapper3d.getCameraRotationFactor()"
90              (input)="onRotationSliderChange($event.value)"
91              (focus)="$event.target.blur()"
92              color="accent"
93              (mousedown)="onInteractionStart([rotationSlider, rotationSliderIcon])"
94              (mouseup)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSlider></mat-slider>
95            <mat-icon
96              color="accent"
97              matTooltip="Spacing"
98              class="slider-icon material-symbols-outlined"
99              (mouseenter)="onInteractionStart([spacingSlider, spacingSliderIcon])"
100              (mouseleave)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSliderIcon> format_letter_spacing </mat-icon>
101            <mat-slider
102              class="slider-spacing"
103              step="0.02"
104              min="0.02"
105              max="1"
106              aria-label="units"
107              [value]="getZSpacingFactor()"
108              (input)="onSeparationSliderChange($event.value)"
109              (focus)="$event.target.blur()"
110              color="accent"
111              (mousedown)="onInteractionStart([spacingSlider, spacingSliderIcon])"
112              (mouseup)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSlider></mat-slider>
113          </div>
114
115          <div class="icon-divider"></div>
116
117          <button
118            color="accent"
119            (mouseenter)="onInteractionStart([zoomInButton])"
120            (mouseleave)="onInteractionEnd([zoomInButton])"
121            mat-icon-button
122            (click)="onZoomInClick()" #zoomInButton>
123            <mat-icon aria-hidden="true"> zoom_in </mat-icon>
124          </button>
125          <button
126            color="accent"
127            (mouseenter)="onInteractionStart([zoomOutButton])"
128            (mouseleave)="onInteractionEnd([zoomOutButton])"
129            mat-icon-button
130            (click)="onZoomOutClick()" #zoomOutButton>
131            <mat-icon aria-hidden="true"> zoom_out </mat-icon>
132          </button>
133
134          <div class="icon-divider"></div>
135
136          <button
137            color="accent"
138            (mouseenter)="onInteractionStart([resetZoomButton])"
139            (mouseleave)="onInteractionEnd([resetZoomButton])"
140            mat-icon-button
141            matTooltip="Restore camera settings"
142            (click)="resetCamera()" #resetZoomButton>
143            <mat-icon aria-hidden="true"> restore </mat-icon>
144          </button>
145        </div>
146      </div>
147      <div class="filter-controls view-controls">
148        <user-options
149          class="block-filter-controls"
150          [userOptions]="userOptions"
151          [eventType]="ViewerEvents.RectsUserOptionsChange"
152          [traceType]="dependencies[0]"
153          [logCallback]="Analytics.Navigation.logRectSettingsChanged">
154        </user-options>
155
156        <div class="displays-section">
157          <span class="mat-body-1"> {{groupLabel}}: </span>
158          <mat-form-field appearance="none" class="displays-select">
159            <mat-select
160              (selectionChange)="onDisplayChange($event)"
161              [value]="currentDisplay?.name">
162              <mat-option
163                *ngFor="let name of displayNames"
164                [value]="name">
165                {{ name }}
166              </mat-option>
167            </mat-select>
168          </mat-form-field>
169        </div>
170      </div>
171    </div>
172    <mat-divider></mat-divider>
173    <div class="rects-content">
174      <div class="canvas-container">
175        <canvas
176          class="large-rects-canvas"
177          (click)="onRectClick($event)"
178          (dblclick)="onRectDblClick($event)"
179          oncontextmenu="return false"></canvas>
180        <div class="large-rects-labels"></div>
181        <canvas
182          class="mini-rects-canvas"
183          (dblclick)="onMiniRectDblClick($event)"
184          oncontextmenu="return false"></canvas>
185      </div>
186    </div>
187  `,
188  styles: [
189    `
190      .view-header {
191        display: flex;
192        flex-direction: column;
193      }
194      .mat-title {
195        padding-top: 8px;
196      }
197      .right-btn-container {
198        display: flex;
199        align-items: center;
200      }
201      .right-btn-container .mat-slider-horizontal {
202        min-width: 64px !important;
203      }
204      .icon-divider {
205        height: 50%;
206      }
207      .slider-container {
208        padding: 0 5px;
209        display: flex;
210        align-items: center;
211      }
212      .slider-icon {
213        min-width: 18px;
214        width: 18px;
215        height: 18px;
216        line-height: 18px;
217        font-size: 18px;
218      }
219      .filter-controls {
220        justify-content: space-between;
221      }
222      .block-filter-controls {
223        display: flex;
224        flex-direction: row;
225        align-items: baseline;
226      }
227      .displays-section {
228        display: flex;
229        flex-direction: row;
230        align-items: center;
231        width: fit-content;
232        flex-wrap: nowrap;
233      }
234      .displays-select {
235        font-size: 14px;
236        background-color: var(--disabled-color);
237        border-radius: 4px;
238        height: 24px;
239        margin-left: 5px;
240      }
241      .rects-content {
242        height: 100%;
243        display: flex;
244        flex-direction: column;
245        padding: 0px 12px;
246      }
247      .canvas-container {
248        height: 100%;
249        width: 100%;
250        position: relative;
251      }
252      .large-rects-canvas {
253        position: absolute;
254        top: 0;
255        left: 0;
256        width: 100%;
257        height: 100%;
258        cursor: pointer;
259      }
260      .large-rects-labels {
261        position: absolute;
262        top: 0;
263        left: 0;
264        width: 100%;
265        height: 100%;
266        pointer-events: none;
267      }
268      .mini-rects-canvas {
269        cursor: pointer;
270        width: 30%;
271        height: 30%;
272        top: 16px;
273        display: block;
274        position: absolute;
275        z-index: 1000;
276      }
277    `,
278    multlineTooltip,
279    iconDividerStyle,
280    viewerCardInnerStyle,
281  ],
282})
283export class RectsComponent implements OnInit, OnDestroy {
284  Analytics = Analytics;
285  ViewerEvents = ViewerEvents;
286
287  @Input() title = 'title';
288  @Input() zoomFactor = 1;
289  @Input() store?: PersistentStore;
290  @Input() rects: UiRect[] = [];
291  @Input() miniRects: UiRect[] | undefined;
292  @Input() displays: DisplayIdentifier[] = [];
293  @Input() highlightedItem = '';
294  @Input() groupLabel = 'Displays';
295  @Input() isStackBased = false;
296  @Input() shadingModes: ShadingMode[] = [ShadingMode.GRADIENT];
297  @Input() userOptions: UserOptions = {};
298  @Input() dependencies: TraceType[] = [];
299
300  @Output() collapseButtonClicked = new EventEmitter();
301
302  private internalRects: UiRect[] = [];
303  private internalMiniRects?: UiRect[];
304  private storeKeyZSpacingFactor = '';
305  private storeKeyShadingMode = '';
306  private displayNames: string[] = [];
307  private internalDisplays: DisplayIdentifier[] = [];
308  private internalHighlightedItem = '';
309  private currentDisplay: DisplayIdentifier | undefined;
310  private mapper3d: Mapper3D;
311  private largeRectsCanvas?: Canvas;
312  private miniRectsCanvas?: Canvas;
313  private resizeObserver: ResizeObserver;
314  private largeRectsCanvasElement?: HTMLCanvasElement;
315  private miniRectsCanvasElement?: HTMLCanvasElement;
316  private largeRectsLabelsElement?: HTMLElement;
317  private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event);
318  private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event);
319
320  private static readonly ZOOM_SCROLL_RATIO = 0.3;
321
322  constructor(
323    @Inject(ElementRef) private elementRef: ElementRef,
324    @Inject(MatIconRegistry) private matIconRegistry: MatIconRegistry,
325    @Inject(DomSanitizer) private domSanitizer: DomSanitizer,
326  ) {
327    this.mapper3d = new Mapper3D();
328    this.resizeObserver = new ResizeObserver((entries) => {
329      this.drawLargeRectsAndLabels();
330    });
331    this.matIconRegistry.addSvgIcon(
332      'cube_full_shade',
333      this.domSanitizer.bypassSecurityTrustResourceUrl(
334        UrlUtils.getRootUrl() + 'cube_full_shade.svg',
335      ),
336    );
337    this.matIconRegistry.addSvgIcon(
338      'cube_partial_shade',
339      this.domSanitizer.bypassSecurityTrustResourceUrl(
340        UrlUtils.getRootUrl() + 'cube_partial_shade.svg',
341      ),
342    );
343  }
344
345  ngOnInit() {
346    this.mapper3d.setAllowedShadingModes(this.shadingModes);
347
348    const canvasContainer: HTMLElement =
349      this.elementRef.nativeElement.querySelector('.canvas-container');
350    this.resizeObserver.observe(canvasContainer);
351
352    const isDarkMode = () => this.store?.get('dark-mode') === 'true';
353
354    this.largeRectsCanvasElement = canvasContainer.querySelector(
355      '.large-rects-canvas',
356    )! as HTMLCanvasElement;
357    this.largeRectsLabelsElement = assertDefined(
358      canvasContainer.querySelector('.large-rects-labels'),
359    ) as HTMLElement;
360    this.largeRectsCanvas = new Canvas(
361      this.largeRectsCanvasElement,
362      this.largeRectsLabelsElement,
363      isDarkMode,
364    );
365    this.largeRectsCanvasElement.addEventListener('mousedown', (event) =>
366      this.onCanvasMouseDown(event),
367    );
368
369    if (this.store) {
370      this.updateControlsFromStore();
371    }
372
373    this.currentDisplay =
374      this.internalDisplays.length > 0
375        ? this.getFirstDisplayWithRectsOrFirstDisplay(this.internalDisplays)
376        : undefined;
377    this.mapper3d.increaseZoomFactor(this.zoomFactor - 1);
378    this.drawLargeRectsAndLabels();
379
380    this.miniRectsCanvasElement = canvasContainer.querySelector(
381      '.mini-rects-canvas',
382    )! as HTMLCanvasElement;
383    this.miniRectsCanvas = new Canvas(
384      this.miniRectsCanvasElement,
385      undefined,
386      isDarkMode,
387    );
388    if (this.miniRects && this.miniRects.length > 0) {
389      this.drawMiniRects();
390    }
391  }
392
393  blurTab() {
394    (document.activeElement as HTMLElement).blur();
395  }
396
397  ngOnChanges(simpleChanges: SimpleChanges) {
398    if (simpleChanges['highlightedItem']) {
399      this.internalHighlightedItem =
400        simpleChanges['highlightedItem'].currentValue;
401      this.mapper3d.setHighlightedRectId(this.internalHighlightedItem);
402      if (!(simpleChanges['rects'] || simpleChanges['displays'])) {
403        this.drawLargeRectsAndLabels();
404      }
405    }
406    let displayChange = false;
407    if (simpleChanges['displays']) {
408      const curr: DisplayIdentifier[] = simpleChanges['displays'].currentValue;
409      const prev: DisplayIdentifier[] | null =
410        simpleChanges['displays'].previousValue;
411      displayChange =
412        curr.length > 0 &&
413        !curr.every((d, index) => d.displayId === prev?.at(index)?.displayId);
414    }
415    if (simpleChanges['rects']) {
416      this.internalRects = simpleChanges['rects'].currentValue;
417      if (!displayChange) {
418        this.drawLargeRectsAndLabels();
419      }
420    }
421    if (displayChange) {
422      this.onDisplaysChange(simpleChanges['displays']);
423    }
424    if (simpleChanges['miniRects']) {
425      this.internalMiniRects = simpleChanges['miniRects'].currentValue;
426      this.drawMiniRects();
427    }
428  }
429
430  ngOnDestroy() {
431    this.resizeObserver?.disconnect();
432  }
433
434  onDisplaysChange(change: SimpleChange) {
435    const displays = change.currentValue;
436    this.internalDisplays = displays;
437    this.displayNames = this.internalDisplays.map((d) => d.name);
438
439    if (displays.length === 0) {
440      return;
441    }
442
443    if (change.firstChange) {
444      this.updateCurrentDisplay(
445        this.getFirstDisplayWithRectsOrFirstDisplay(this.internalDisplays),
446      );
447      return;
448    }
449
450    const curr = this.internalDisplays.find(
451      (display) => display.displayId === this.currentDisplay?.displayId,
452    );
453    if (curr) {
454      this.updateCurrentDisplay(curr);
455      return;
456    }
457
458    const displaysWithCurrentGroupId = this.internalDisplays.filter(
459      (display) => display.groupId === this.mapper3d.getCurrentGroupId(),
460    );
461    if (displaysWithCurrentGroupId.length === 0) {
462      this.updateCurrentDisplay(
463        this.getFirstDisplayWithRectsOrFirstDisplay(this.internalDisplays),
464      );
465      return;
466    }
467
468    this.updateCurrentDisplay(
469      this.getFirstDisplayWithRectsOrFirstDisplay(displaysWithCurrentGroupId),
470    );
471    return;
472  }
473
474  updateControlsFromStore() {
475    this.storeKeyZSpacingFactor = `rectsView.${this.title}.zSpacingFactor`;
476    this.storeKeyShadingMode = `rectsView.${this.title}.shadingMode`;
477
478    const storedZSpacingFactor = assertDefined(this.store).get(
479      this.storeKeyZSpacingFactor,
480    );
481    if (storedZSpacingFactor !== undefined) {
482      this.mapper3d.setZSpacingFactor(Number(storedZSpacingFactor));
483    }
484
485    const storedShadingMode = assertDefined(this.store).get(
486      this.storeKeyShadingMode,
487    );
488    if (
489      storedShadingMode !== undefined &&
490      this.shadingModes.includes(storedShadingMode as ShadingMode)
491    ) {
492      this.mapper3d.setShadingMode(storedShadingMode as ShadingMode);
493    }
494  }
495
496  onSeparationSliderChange(factor: number) {
497    Analytics.Navigation.logRectSettingsChanged(
498      'z spacing',
499      factor,
500      TRACE_INFO[this.dependencies[0]].name,
501    );
502    this.store?.add(this.storeKeyZSpacingFactor, `${factor}`);
503    this.mapper3d.setZSpacingFactor(factor);
504    this.drawLargeRectsAndLabels();
505  }
506
507  onRotationSliderChange(factor: number) {
508    this.mapper3d.setCameraRotationFactor(factor);
509    this.drawLargeRectsAndLabels();
510  }
511
512  resetCamera() {
513    Analytics.Navigation.logZoom('reset', 'rects');
514    this.mapper3d.resetCamera();
515    this.drawLargeRectsAndLabels();
516  }
517
518  @HostListener('wheel', ['$event'])
519  onScroll(event: WheelEvent) {
520    if ((event.target as HTMLElement).className === 'large-rects-canvas') {
521      if (event.deltaY > 0) {
522        Analytics.Navigation.logZoom('scroll', 'rects', 'out');
523        this.doZoomOut(RectsComponent.ZOOM_SCROLL_RATIO);
524      } else {
525        Analytics.Navigation.logZoom('scroll', 'rects', 'in');
526        this.doZoomIn(RectsComponent.ZOOM_SCROLL_RATIO);
527      }
528    }
529  }
530
531  onCanvasMouseDown(event: MouseEvent) {
532    document.addEventListener('mousemove', this.mouseMoveListener);
533    document.addEventListener('mouseup', this.mouseUpListener);
534  }
535
536  onMouseMove(event: MouseEvent) {
537    const distance = new Distance2D(event.movementX, event.movementY);
538    this.mapper3d.addPanScreenDistance(distance);
539    this.drawLargeRectsAndLabels();
540  }
541
542  onMouseUp(event: MouseEvent) {
543    document.removeEventListener('mousemove', this.mouseMoveListener);
544    document.removeEventListener('mouseup', this.mouseUpListener);
545  }
546
547  onZoomInClick() {
548    Analytics.Navigation.logZoom('button', 'rects', 'in');
549    this.doZoomIn();
550  }
551
552  onZoomOutClick() {
553    Analytics.Navigation.logZoom('button', 'rects', 'out');
554    this.doZoomOut();
555  }
556
557  onDisplayChange(event: MatSelectChange) {
558    const displayName = event.value;
559    const display = assertDefined(
560      this.internalDisplays.find((d) => d.name === displayName),
561    );
562    this.updateCurrentDisplay(display);
563    const viewerEvent = new CustomEvent(ViewerEvents.RectGroupIdChange, {
564      bubbles: true,
565      detail: {groupId: display.groupId},
566    });
567    this.elementRef.nativeElement.dispatchEvent(viewerEvent);
568  }
569
570  onRectClick(event: MouseEvent) {
571    event.preventDefault();
572
573    const id = this.findClickedRectId(event);
574    if (id !== undefined) {
575      this.notifyHighlightedItem(id);
576    }
577  }
578
579  onRectDblClick(event: MouseEvent) {
580    event.preventDefault();
581
582    const clickedRectId = this.findClickedRectId(event);
583    if (clickedRectId === undefined) {
584      return;
585    }
586
587    this.elementRef.nativeElement.dispatchEvent(
588      new CustomEvent(ViewerEvents.RectsDblClick, {
589        bubbles: true,
590        detail: new RectDblClickDetail(clickedRectId),
591      }),
592    );
593  }
594
595  onMiniRectDblClick(event: MouseEvent) {
596    event.preventDefault();
597
598    this.elementRef.nativeElement.dispatchEvent(
599      new CustomEvent(ViewerEvents.MiniRectsDblClick, {bubbles: true}),
600    );
601  }
602
603  getZSpacingFactor(): number {
604    return this.mapper3d.getZSpacingFactor();
605  }
606
607  getShadingMode(): ShadingMode {
608    return this.mapper3d.getShadingMode();
609  }
610
611  onShadingModeButtonClicked() {
612    this.mapper3d.updateShadingMode();
613    const newMode = this.mapper3d.getShadingMode();
614    Analytics.Navigation.logRectSettingsChanged(
615      'shading mode',
616      newMode,
617      TRACE_INFO[this.dependencies[0]].name,
618    );
619    this.store?.add(this.storeKeyShadingMode, newMode);
620    this.drawLargeRectsAndLabels();
621  }
622
623  onInteractionStart(components: CanColor[]) {
624    components.forEach((c) => (c.color = 'primary'));
625  }
626
627  onInteractionEnd(components: CanColor[]) {
628    components.forEach((c) => (c.color = 'accent'));
629  }
630
631  private getFirstDisplayWithRectsOrFirstDisplay(
632    displays: DisplayIdentifier[],
633  ): DisplayIdentifier {
634    return (
635      displays.find((display) =>
636        this.internalRects.some(
637          (rect) => !rect.isDisplay && rect.groupId === display.groupId,
638        ),
639      ) ?? assertDefined(displays.at(0))
640    );
641  }
642
643  private updateCurrentDisplay(display: DisplayIdentifier) {
644    this.currentDisplay = display;
645    this.mapper3d.setCurrentGroupId(display.groupId);
646    this.drawLargeRectsAndLabels();
647  }
648
649  private findClickedRectId(event: MouseEvent): string | undefined {
650    const canvas = event.target as Element;
651    const canvasOffset = canvas.getBoundingClientRect();
652
653    const x =
654      ((event.clientX - canvasOffset.left) / canvas.clientWidth) * 2 - 1;
655    const y =
656      -((event.clientY - canvasOffset.top) / canvas.clientHeight) * 2 + 1;
657    const z = 0;
658
659    return this.largeRectsCanvas?.getClickedRectId(x, y, z);
660  }
661
662  private doZoomIn(ratio = 1) {
663    this.mapper3d.increaseZoomFactor(ratio);
664    this.drawLargeRectsAndLabels();
665  }
666
667  private doZoomOut(ratio = 1) {
668    this.mapper3d.decreaseZoomFactor(ratio);
669    this.drawLargeRectsAndLabels();
670  }
671
672  private drawLargeRectsAndLabels() {
673    // TODO(b/258593034): Re-create scene only when input rects change. With the other input events
674    // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
675    // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
676    // work directly with three.js's meshes.
677    this.mapper3d.setRects(this.internalRects);
678    this.largeRectsCanvas?.draw(this.mapper3d.computeScene());
679  }
680
681  private drawMiniRects() {
682    // TODO(b/258593034): Re-create scene only when input rects change. With the other input events
683    // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
684    // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
685    // work directly with three.js's meshes.
686    if (this.internalMiniRects) {
687      const largeRectShadingMode = this.mapper3d.getShadingMode();
688      const largeRectGroupId = this.mapper3d.getCurrentGroupId();
689      const largeRectZSpacing = this.mapper3d.getZSpacingFactor();
690      const largeRectCameraRotation = this.mapper3d.getCameraRotationFactor();
691
692      this.mapper3d.setShadingMode(ShadingMode.GRADIENT);
693      this.mapper3d.setCurrentGroupId(this.internalMiniRects[0]?.groupId);
694      this.mapper3d.resetToOrthogonalState();
695
696      this.mapper3d.setRects(this.internalMiniRects);
697      this.mapper3d.decreaseZoomFactor(this.zoomFactor - 1);
698      this.miniRectsCanvas?.draw(this.mapper3d.computeScene());
699      this.mapper3d.increaseZoomFactor(this.zoomFactor - 1);
700
701      // Mapper internally sets these values to 100%. They need to be reset afterwards
702      if (this.miniRectsCanvasElement) {
703        this.miniRectsCanvasElement.style.width = '25%';
704        this.miniRectsCanvasElement.style.height = '25%';
705      }
706
707      this.mapper3d.setShadingMode(largeRectShadingMode);
708      this.mapper3d.setCurrentGroupId(largeRectGroupId);
709      this.mapper3d.setZSpacingFactor(largeRectZSpacing);
710      this.mapper3d.setCameraRotationFactor(largeRectCameraRotation);
711    }
712  }
713
714  private notifyHighlightedItem(id: string) {
715    const event: CustomEvent = new CustomEvent(
716      ViewerEvents.HighlightedIdChange,
717      {
718        bubbles: true,
719        detail: {id},
720      },
721    );
722    this.elementRef.nativeElement.dispatchEvent(event);
723  }
724}
725