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