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