• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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