blob: 71433f2ffab4a2733500561bd1e01dbac5d5975e [file] [log] [blame]
/*
* Copyright (C) 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {Component, Input, OnDestroy, Inject, ElementRef, SimpleChanges, OnInit} from "@angular/core";
import {Rectangle} from "viewers/common/rectangle";
import {Canvas} from "./canvas";
import {ViewerEvents} from "viewers/common/viewer_events";
import {Mapper3D} from "./mapper3d";
@Component({
selector: "rects-view",
template: `
<div class="view-controls">
<h2 class="mat-title">{{title}}</h2>
<div class="top-view-controls">
<mat-checkbox
color="primary"
[checked]="mapper3d.getShowOnlyVisibleMode()"
(change)="onShowOnlyVisibleModeChange($event.checked!)"
>Only visible
</mat-checkbox>
<mat-checkbox
color="primary"
[disabled]="mapper3d.getShowOnlyVisibleMode()"
[checked]="mapper3d.getShowVirtualMode()"
(change)="onShowVirtualModeChange($event.checked!)"
>Show virtual
</mat-checkbox>
<div class="right-btn-container">
<button color="primary" mat-icon-button (click)="onZoomInClick()">
<mat-icon aria-hidden="true">
zoom_in
</mat-icon>
</button>
<button color="primary" mat-icon-button (click)="onZoomOutClick()">
<mat-icon aria-hidden="true">
zoom_out
</mat-icon>
</button>
<button
color="primary"
mat-icon-button
matTooltip="Restore camera settings"
(click)="resetCamera()"
>
<mat-icon aria-hidden="true">
restore
</mat-icon>
</button>
</div>
</div>
<div class="slider-view-controls">
<div class="slider-container">
<p class="slider-label mat-body-2">Rotation</p>
<mat-slider
class="slider-rotation"
step="0.02"
min="0"
max="1"
aria-label="units"
[value]="mapper3d.getCameraRotationFactor()"
(input)="onRotationSliderChange($event.value!)"
color="primary"
></mat-slider>
</div>
<div class="slider-container">
<p class="slider-label mat-body-2">Spacing</p>
<mat-slider
class="slider-spacing"
step="0.02"
min="0.02"
max="1"
aria-label="units"
[value]="mapper3d.getZSpacingFactor()"
(input)="onSeparationSliderChange($event.value!)"
color="primary"
></mat-slider>
</div>
</div>
</div>
<mat-divider></mat-divider>
<div class="rects-content">
<div class="canvas-container">
<canvas class="canvas-rects" (click)="onRectClick($event)"
oncontextmenu="return false">
</canvas>
<div class="canvas-labels">
</div>
</div>
<div *ngIf="wrappedDisplayIds.length > 1"
class="display-button-container">
<button
*ngFor="let displayId of wrappedDisplayIds"
color="primary"
mat-raised-button
(click)="onDisplayIdChange(displayId)"
>{{displayId}}</button>
</div>
</div>
`,
styles: [
`
.view-controls {
display: flex;
flex-direction: column;
}
.top-view-controls, .slider-view-controls {
display: flex;
flex-direction: row;
flex-wrap: wrap;
column-gap: 10px;
align-items: center;
margin-bottom: 12px;
}
.right-btn-container {
margin-left: auto;
}
.slider-view-controls {
justify-content: space-between;
}
.slider-container {
position: relative;
}
.slider-label {
position: absolute;
top: 0;
}
.rects-content {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
}
.canvas-container {
height: 100%;
width: 100%;
position: relative;
}
.canvas-rects {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.canvas-labels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.display-button-container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
column-gap: 10px;
}
`
]
})
export class RectsComponent implements OnInit, OnDestroy {
@Input() title = "title";
@Input() set rects(rects: Rectangle[]) {
this.wrappedRects = rects;
this.drawScene();
}
@Input() set displayIds(ids: number[]) {
this.wrappedDisplayIds = ids;
if (!this.wrappedDisplayIds.includes(this.mapper3d.getCurrentDisplayId())) {
this.mapper3d.setCurrentDisplayId(this.wrappedDisplayIds[0]);
this.drawScene();
}
}
@Input() set highlightedItems(ids: string[]) {
this.wrappedHighlightedItems = ids.map(id => Number(id));
this.mapper3d.setHighlightedRectIds(this.wrappedHighlightedItems);
this.drawScene();
}
private wrappedRects: Rectangle[] = [];
private wrappedDisplayIds: number[] = [];
private wrappedHighlightedItems: number[] = [];
private mapper3d: Mapper3D;
private canvas?: Canvas;
private resizeObserver: ResizeObserver;
private canvasRects?: HTMLCanvasElement;
private canvasLabels?: HTMLElement;
constructor(
@Inject(ElementRef) private elementRef: ElementRef,
) {
this.mapper3d = new Mapper3D();
this.resizeObserver = new ResizeObserver((entries) => {
this.drawScene();
});
}
ngOnInit() {
const canvasContainer = this.elementRef.nativeElement.querySelector(".canvas-container");
this.resizeObserver.observe(canvasContainer);
this.canvasRects = canvasContainer.querySelector(".canvas-rects")! as HTMLCanvasElement;
this.canvasLabels = canvasContainer.querySelector(".canvas-labels");
this.canvas = new Canvas(this.canvasRects, this.canvasLabels!);
this.mapper3d.setCurrentDisplayId(this.wrappedDisplayIds[0] ?? 0);
this.drawScene();
}
ngOnDestroy() {
this.resizeObserver?.disconnect();
}
public onSeparationSliderChange(factor: number) {
this.mapper3d.setZSpacingFactor(factor);
this.drawScene();
}
public onRotationSliderChange(factor: number) {
this.mapper3d.setCameraRotationFactor(factor);
this.drawScene();
}
public resetCamera() {
this.mapper3d.resetCamera();
this.drawScene();
}
public onZoomInClick() {
this.mapper3d.increaseZoomFactor();
this.drawScene();
}
public onZoomOutClick() {
this.mapper3d.decreaseZoomFactor();
this.drawScene();
}
public onShowOnlyVisibleModeChange(enabled: boolean) {
this.mapper3d.setShowOnlyVisibleMode(enabled);
this.drawScene();
}
public onShowVirtualModeChange(enabled: boolean) {
this.mapper3d.setShowVirtualMode(enabled);
this.drawScene();
}
public onDisplayIdChange(id: number) {
this.mapper3d.setCurrentDisplayId(id);
this.drawScene();
}
public onRectClick(event: MouseEvent) {
event.preventDefault();
const canvas = (event.target as Element);
const canvasOffset = canvas.getBoundingClientRect();
const x = ((event.clientX-canvasOffset.left)/canvas.clientWidth) * 2 - 1;
const y = -((event.clientY-canvasOffset.top)/canvas.clientHeight) * 2 + 1;
const z = 0;
const id = this.canvas?.getClickedRectId(x, y, z);
if (id !== undefined) {
this.notifyHighlightedItem(id);
}
}
private drawScene() {
// TODO: Re-create scene only when input rects change. With the other input events
// (rotation, spacing, ...) we can just update the camera and/or update the mesh positions.
// We'd probably need to get rid of the intermediate layer (Scene3D, Rect3D, ... types) and
// work directly with three.js's meshes.
this.mapper3d.setRects(this.wrappedRects);
this.canvas?.draw(this.mapper3d.computeScene());
}
private notifyHighlightedItem(id: string) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HighlightedChange,
{
bubbles: true,
detail: { id: id }
});
this.elementRef.nativeElement.dispatchEvent(event);
}
}