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 */ 16import {Component, ElementRef, HostListener, Inject, Input, OnDestroy, OnInit} from '@angular/core'; 17import {Rectangle} from 'viewers/common/rectangle'; 18import {ViewerEvents} from 'viewers/common/viewer_events'; 19import {Canvas} from './canvas'; 20import {Mapper3D} from './mapper3d'; 21import {Distance2D} from './types3d'; 22 23@Component({ 24 selector: 'rects-view', 25 template: ` 26 <div class="view-controls"> 27 <h2 class="mat-title">{{ title }}</h2> 28 <div class="top-view-controls"> 29 <mat-checkbox 30 color="primary" 31 [checked]="mapper3d.getShowOnlyVisibleMode()" 32 (change)="onShowOnlyVisibleModeChange($event.checked!)" 33 >Only visible 34 </mat-checkbox> 35 <mat-checkbox 36 *ngIf="enableShowVirtualButton" 37 color="primary" 38 [disabled]="mapper3d.getShowOnlyVisibleMode()" 39 [checked]="mapper3d.getShowVirtualMode()" 40 (change)="onShowVirtualModeChange($event.checked!)" 41 >Show virtual 42 </mat-checkbox> 43 <div class="right-btn-container"> 44 <button color="primary" mat-icon-button (click)="onZoomInClick()"> 45 <mat-icon aria-hidden="true"> zoom_in </mat-icon> 46 </button> 47 <button color="primary" mat-icon-button (click)="onZoomOutClick()"> 48 <mat-icon aria-hidden="true"> zoom_out </mat-icon> 49 </button> 50 <button 51 color="primary" 52 mat-icon-button 53 matTooltip="Restore camera settings" 54 (click)="resetCamera()"> 55 <mat-icon aria-hidden="true"> restore </mat-icon> 56 </button> 57 </div> 58 </div> 59 <div class="slider-view-controls"> 60 <div class="slider-container"> 61 <p class="slider-label mat-body-2">Rotation</p> 62 <mat-slider 63 class="slider-rotation" 64 step="0.02" 65 min="0" 66 max="1" 67 aria-label="units" 68 [value]="mapper3d.getCameraRotationFactor()" 69 (input)="onRotationSliderChange($event.value!)" 70 color="primary"></mat-slider> 71 </div> 72 <div class="slider-container"> 73 <p class="slider-label mat-body-2">Spacing</p> 74 <mat-slider 75 class="slider-spacing" 76 step="0.02" 77 min="0.02" 78 max="1" 79 aria-label="units" 80 [value]="mapper3d.getZSpacingFactor()" 81 (input)="onSeparationSliderChange($event.value!)" 82 color="primary"></mat-slider> 83 </div> 84 </div> 85 </div> 86 <mat-divider></mat-divider> 87 <div class="rects-content"> 88 <div class="canvas-container"> 89 <canvas class="canvas-rects" (click)="onRectClick($event)" oncontextmenu="return false"> 90 </canvas> 91 <div class="canvas-labels"></div> 92 </div> 93 <div *ngIf="internalDisplayIds.length > 1" class="display-button-container"> 94 <button 95 *ngFor="let displayId of internalDisplayIds" 96 color="primary" 97 mat-raised-button 98 (click)="onDisplayIdChange(displayId)"> 99 {{ displayId }} 100 </button> 101 </div> 102 </div> 103 `, 104 styles: [ 105 ` 106 .view-controls { 107 display: flex; 108 flex-direction: column; 109 } 110 .top-view-controls, 111 .slider-view-controls { 112 display: flex; 113 flex-direction: row; 114 flex-wrap: wrap; 115 column-gap: 10px; 116 align-items: center; 117 margin-bottom: 12px; 118 } 119 .right-btn-container { 120 margin-left: auto; 121 } 122 .slider-view-controls { 123 justify-content: space-between; 124 } 125 .slider-container { 126 position: relative; 127 } 128 .slider-label { 129 position: absolute; 130 top: 0; 131 } 132 .rects-content { 133 height: 100%; 134 width: 100%; 135 display: flex; 136 flex-direction: column; 137 } 138 .canvas-container { 139 height: 100%; 140 width: 100%; 141 position: relative; 142 } 143 .canvas-rects { 144 position: absolute; 145 top: 0; 146 left: 0; 147 width: 100%; 148 height: 100%; 149 cursor: pointer; 150 } 151 .canvas-labels { 152 position: absolute; 153 top: 0; 154 left: 0; 155 width: 100%; 156 height: 100%; 157 pointer-events: none; 158 } 159 .display-button-container { 160 display: flex; 161 flex-direction: row; 162 flex-wrap: wrap; 163 column-gap: 10px; 164 } 165 `, 166 ], 167}) 168export class RectsComponent implements OnInit, OnDestroy { 169 @Input() title = 'title'; 170 @Input() enableShowVirtualButton: boolean = true; 171 @Input() set rects(rects: Rectangle[]) { 172 this.internalRects = rects; 173 this.drawScene(); 174 } 175 176 @Input() set displayIds(ids: number[]) { 177 this.internalDisplayIds = ids; 178 if (!this.internalDisplayIds.includes(this.mapper3d.getCurrentDisplayId())) { 179 this.mapper3d.setCurrentDisplayId(this.internalDisplayIds[0]); 180 this.drawScene(); 181 } 182 } 183 184 @Input() set highlightedItems(stableIds: string[]) { 185 this.internalHighlightedItems = stableIds; 186 this.mapper3d.setHighlightedRectIds(this.internalHighlightedItems); 187 this.drawScene(); 188 } 189 190 private internalRects: Rectangle[] = []; 191 private internalDisplayIds: number[] = []; 192 private internalHighlightedItems: string[] = []; 193 194 private mapper3d: Mapper3D; 195 private canvas?: Canvas; 196 private resizeObserver: ResizeObserver; 197 private canvasRects?: HTMLCanvasElement; 198 private canvasLabels?: HTMLElement; 199 private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event); 200 private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event); 201 202 constructor(@Inject(ElementRef) private elementRef: ElementRef) { 203 this.mapper3d = new Mapper3D(); 204 this.resizeObserver = new ResizeObserver((entries) => { 205 this.drawScene(); 206 }); 207 } 208 209 ngOnInit() { 210 const canvasContainer = this.elementRef.nativeElement.querySelector('.canvas-container'); 211 this.resizeObserver.observe(canvasContainer); 212 213 this.canvasRects = canvasContainer.querySelector('.canvas-rects')! as HTMLCanvasElement; 214 this.canvasLabels = canvasContainer.querySelector('.canvas-labels'); 215 this.canvas = new Canvas(this.canvasRects, this.canvasLabels!); 216 217 this.canvasRects.addEventListener('mousedown', (event) => this.onCanvasMouseDown(event)); 218 219 this.mapper3d.setCurrentDisplayId(this.internalDisplayIds[0] ?? 0); 220 this.drawScene(); 221 } 222 223 ngOnDestroy() { 224 this.resizeObserver?.disconnect(); 225 } 226 227 onSeparationSliderChange(factor: number) { 228 this.mapper3d.setZSpacingFactor(factor); 229 this.drawScene(); 230 } 231 232 onRotationSliderChange(factor: number) { 233 this.mapper3d.setCameraRotationFactor(factor); 234 this.drawScene(); 235 } 236 237 resetCamera() { 238 this.mapper3d.resetCamera(); 239 this.drawScene(); 240 } 241 242 @HostListener('wheel', ['$event']) 243 onScroll(event: WheelEvent) { 244 if (event.deltaY > 0) { 245 this.doZoomOut(); 246 } else { 247 this.doZoomIn(); 248 } 249 } 250 251 onCanvasMouseDown(event: MouseEvent) { 252 document.addEventListener('mousemove', this.mouseMoveListener); 253 document.addEventListener('mouseup', this.mouseUpListener); 254 } 255 256 onMouseMove(event: MouseEvent) { 257 const distance = new Distance2D(event.movementX, event.movementY); 258 this.mapper3d.addPanScreenDistance(distance); 259 this.drawScene(); 260 } 261 262 onMouseUp(event: MouseEvent) { 263 document.removeEventListener('mousemove', this.mouseMoveListener); 264 document.removeEventListener('mouseup', this.mouseUpListener); 265 } 266 267 onZoomInClick() { 268 this.doZoomIn(); 269 } 270 271 onZoomOutClick() { 272 this.doZoomOut(); 273 } 274 275 onShowOnlyVisibleModeChange(enabled: boolean) { 276 this.mapper3d.setShowOnlyVisibleMode(enabled); 277 this.drawScene(); 278 } 279 280 onShowVirtualModeChange(enabled: boolean) { 281 this.mapper3d.setShowVirtualMode(enabled); 282 this.drawScene(); 283 } 284 285 onDisplayIdChange(id: number) { 286 this.mapper3d.setCurrentDisplayId(id); 287 this.drawScene(); 288 } 289 290 onRectClick(event: MouseEvent) { 291 event.preventDefault(); 292 293 const canvas = event.target as Element; 294 const canvasOffset = canvas.getBoundingClientRect(); 295 296 const x = ((event.clientX - canvasOffset.left) / canvas.clientWidth) * 2 - 1; 297 const y = -((event.clientY - canvasOffset.top) / canvas.clientHeight) * 2 + 1; 298 const z = 0; 299 300 const id = this.canvas?.getClickedRectId(x, y, z); 301 if (id !== undefined) { 302 this.notifyHighlightedItem(id); 303 } 304 } 305 306 private doZoomIn() { 307 this.mapper3d.increaseZoomFactor(); 308 this.drawScene(); 309 } 310 311 private doZoomOut() { 312 this.mapper3d.decreaseZoomFactor(); 313 this.drawScene(); 314 } 315 316 private drawScene() { 317 // TODO: Re-create scene only when input rects change. With the other input events 318 // (rotation, spacing, ...) we can just update the camera and/or update the mesh positions. 319 // We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and 320 // work directly with three.js's meshes. 321 this.mapper3d.setRects(this.internalRects); 322 this.canvas?.draw(this.mapper3d.computeScene()); 323 } 324 325 private notifyHighlightedItem(id: string) { 326 const event: CustomEvent = new CustomEvent(ViewerEvents.HighlightedChange, { 327 bubbles: true, 328 detail: {id}, 329 }); 330 this.elementRef.nativeElement.dispatchEvent(event); 331 } 332} 333