blob: 81a2d8d2b3c2de36d15e81969025f290afaf941a [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 {CommonModule} from '@angular/common';
import {
Component,
ElementRef,
EventEmitter,
HostListener,
Inject,
Input,
OnDestroy,
OnInit,
Output,
SimpleChange,
SimpleChanges,
} from '@angular/core';
import {MatButtonModule} from '@angular/material/button';
import {
MatButtonToggleChange,
MatButtonToggleModule,
} from '@angular/material/button-toggle';
import {MatDividerModule} from '@angular/material/divider';
import {MatFormFieldModule} from '@angular/material/form-field';
import {MatIconModule, MatIconRegistry} from '@angular/material/icon';
import {MatInputModule} from '@angular/material/input';
import {MatSelectChange, MatSelectModule} from '@angular/material/select';
import {MatSliderModule} from '@angular/material/slider';
import {MatTooltipModule} from '@angular/material/tooltip';
import {DomSanitizer} from '@angular/platform-browser';
import {assertDefined} from 'common/assert_utils';
import {Distance} from 'common/geometry/distance';
import {PersistentStore} from 'common/store/persistent_store';
import {getRootUrl} from 'common/window';
import {Analytics} from 'logging/analytics';
import {TRACE_INFO} from 'trace_api/trace_info';
import {TraceType} from 'trace_api/trace_type';
import {DisplayIdentifier} from 'viewers/common/display_identifier';
import {UiHierarchyTreeNode} from 'viewers/common/ui_hierarchy_tree_node';
import {UserOptions} from 'viewers/common/user_options';
import {RectDblClickDetail, ViewerEvents} from 'viewers/common/viewer_events';
import {CollapsibleSectionTitleComponent} from 'viewers/components/collapsible_section_title_component';
import {RectSpec, TraceRectType} from 'viewers/components/rects/rect_spec';
import {UiRect} from 'viewers/components/rects/ui_rect';
import {iconDividerStyle} from 'viewers/components/styles/icon_divider.styles';
import {multlineTooltip} from 'viewers/components/styles/tooltip.styles';
import {viewerCardInnerStyle} from 'viewers/components/styles/viewer_card.styles';
import {UserOptionsComponent} from 'viewers/components/user_options_component';
import {Canvas} from './canvas';
import {Mapper3D} from './mapper3d';
import {ShadingMode} from './shading_mode';
interface CanColor {
color: string | undefined;
}
@Component({
selector: 'rects-view',
standalone: true,
imports: [
CommonModule,
MatButtonModule,
MatButtonToggleModule,
MatDividerModule,
MatIconModule,
MatSelectModule,
MatFormFieldModule,
MatInputModule,
MatSliderModule,
MatTooltipModule,
CollapsibleSectionTitleComponent,
UserOptionsComponent,
],
template: `
<div class="view-header">
<div class="title-section">
<collapsible-section-title
[title]="title"
(collapseButtonClicked)="collapseButtonClicked.emit()"></collapsible-section-title>
<div class="right-btn-container">
<button
color="accent"
class="shading-mode"
(mouseenter)="onInteractionStart([shadingModeButton])"
(mouseleave)="onInteractionEnd([shadingModeButton])"
mat-icon-button
[matTooltip]="getShadingMode()"
[disabled]="shadingModes.length < 2"
(click)="onShadingModeButtonClicked()" #shadingModeButton>
@if (largeRectsMapper3d.isWireFrame()) {
<mat-icon class="material-symbols-outlined" aria-hidden="true"> deployed_code </mat-icon>
} @else if (largeRectsMapper3d.isShadedByGradient()) {
<mat-icon svgIcon="cube_partial_shade"></mat-icon>
} @else if (largeRectsMapper3d.isShadedByOpacity()) {
<mat-icon svgIcon="cube_full_shade"></mat-icon>
}
</button>
<div class="icon-divider"></div>
<div class="slider-container">
<mat-icon
color="accent"
matTooltip="Rotation"
class="slider-icon"
(mouseenter)="onInteractionStart([rotationSlider, rotationSliderIcon])"
(mouseleave)="onInteractionEnd([rotationSlider, rotationSliderIcon])" #rotationSliderIcon> rotate_90_degrees_ccw </mat-icon>
<mat-slider
class="slider-rotation"
aria-label="units"
color="accent"
[step]="0.02"
[min]="0"
[max]="1"
(mousedown)="onInteractionStart([rotationSlider, rotationSliderIcon])"
(mouseup)="onInteractionEnd([rotationSlider, rotationSliderIcon])"
#rotationSlider>
<input
[value]="largeRectsMapper3d.getCameraRotationFactor()"
(input)="onRotationSliderChange($event.target.value)"
(focus)="$event.target.blur()"
matSliderThumb>
</mat-slider>
<mat-icon
color="accent"
matTooltip="Spacing"
class="slider-icon material-symbols-outlined"
(mouseenter)="onInteractionStart([spacingSlider, spacingSliderIcon])"
(mouseleave)="onInteractionEnd([spacingSlider, spacingSliderIcon])" #spacingSliderIcon> format_letter_spacing </mat-icon>
<mat-slider
class="slider-spacing"
aria-label="units"
color="accent"
[step]="0.02"
[min]="0.02"
[max]="1"
(mousedown)="onInteractionStart([spacingSlider, spacingSliderIcon])"
(mouseup)="onInteractionEnd([spacingSlider, spacingSliderIcon])"
#spacingSlider>
<input
[value]="getZSpacingFactor()"
(input)="onSeparationSliderChange($event.target.value)"
(focus)="$event.target.blur()"
matSliderThumb>
</mat-slider>
</div>
<div class="icon-divider"></div>
<button
color="accent"
(mouseenter)="onInteractionStart([zoomInButton])"
(mouseleave)="onInteractionEnd([zoomInButton])"
mat-icon-button
class="zoom-in-button"
(click)="onZoomInClick()" #zoomInButton>
<mat-icon aria-hidden="true"> zoom_in </mat-icon>
</button>
<button
color="accent"
(mouseenter)="onInteractionStart([zoomOutButton])"
(mouseleave)="onInteractionEnd([zoomOutButton])"
mat-icon-button
class="zoom-out-button"
(click)="onZoomOutClick()" #zoomOutButton>
<mat-icon aria-hidden="true"> zoom_out </mat-icon>
</button>
<div class="icon-divider"></div>
<button
color="accent"
(mouseenter)="onInteractionStart([resetZoomButton])"
(mouseleave)="onInteractionEnd([resetZoomButton])"
mat-icon-button
matTooltip="Restore camera settings"
class="reset-button"
(click)="resetCamera()" #resetZoomButton>
<mat-icon aria-hidden="true"> restore </mat-icon>
</button>
</div>
</div>
<div class="filter-controls view-controls">
<user-options
class="block-filter-controls"
[userOptions]="userOptions"
[eventType]="ViewerEvents.RectsUserOptionsChange"
[traceType]="dependencies[0]"
[logCallback]="Analytics.Navigation.logRectSettingsChanged">
</user-options>
<div class="displays-section">
@if (allRectSpecs) {
<mat-button-toggle-group
[value]="rectSpec"
(change)="onRectTypeButtonClicked($event)"
appearance="legacy"
class="rect-type-toggle"
[hideSingleSelectionIndicator]="true">
@for (spec of allRectSpecs; track spec) {
<mat-button-toggle [value]="spec">
<mat-icon
[color]="spec === rectSpec ? 'primary' : 'accent'"
[matTooltip]="'Show ' + spec.type"
class="rect-type-icon material-symbols-outlined">{{spec.icon}}</mat-icon>
</mat-button-toggle>
}
</mat-button-toggle-group>
}
<span class="mat-body-1">{{groupLabel}}:</span>
<mat-form-field
class="displays-select"
subscriptSizing="dynamic"
appearance="outline">
<mat-select
#displaySelect
disableOptionCentering
(selectionChange)="onDisplaySelectChange($event)"
[value]="currentDisplays"
[disabled]="internalDisplays.length === 1"
panelWidth="340px"
multiple>
<mat-select-trigger>
<span>
{{ getSelectTriggerValue() }}
</span>
</mat-select-trigger>
@for (display of internalDisplays; track display) {
<mat-option
[value]="display"
[matTooltip]="'Display Id: ' + display.displayId"
matTooltipPosition="right">
<div class="option-with-chip">
<button
mat-flat-button
class="option-only-button"
(click)="onOnlyButtonClick($event, display)">Only</button>
<span class="option-label-text text-no-overflow">{{ display.name }}</span>
</div>
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
</div>
<mat-divider></mat-divider>
@if (showRectSpecWarning()) {
<span
class="mat-body-1 warning">
<mat-icon class="warning-icon"> warning </mat-icon>
<span class="warning-message text-no-overflow">
Showing {{rectSpec.type}} - change rect type via toggle above
</span>
</span>
}
@if (rects.length===0) {
<span class="mat-body-1 placeholder-text"> No rects found. </span>
}
@if (currentDisplays.length===0) {
<span class="mat-body-1 placeholder-text"> No displays selected. </span>
}
<div class="rects-content">
<div class="canvas-container">
<canvas
class="large-rects-canvas"
(click)="onRectClick($event)"
(dblclick)="onRectDblClick($event)"
oncontextmenu="return false"></canvas>
<div class="large-rects-labels"></div>
<canvas
class="mini-rects-canvas"
(dblclick)="onMiniRectDblClick($event)"
oncontextmenu="return false"></canvas>
</div>
</div>
@if (rectSpec) {
<span class="mat-body-1 rect-legend">
<span class="shading-opts" [class.force-show-all]="legendExpanded" #shadingOpts>
@for (opt of rectSpec.legend; track opt) {
@if (!largeRectsMapper3d.isWireFrame() || opt.showInWireFrameMode) {
<span
class="shading-opt">
@if (opt.fill === undefined) {
<mat-icon
[style.border-color]="opt.border"
class="square">question_mark</mat-icon>
}
@if (opt.fill !== undefined) {
<div
[style.background-color]="opt.fill"
[style.border-color]="opt.border"
class="square"></div>
}
<span class="mat-body-1 shading-opt-desc">{{opt.desc}}</span>
</span>
}
}
</span>
@if (showExpandButton(shadingOpts)) {
<button
mat-icon-button
class="rect-legend-expand-button"
(click)="legendExpanded = !legendExpanded">
<mat-icon class="material-symbols-outlined">{{legendExpanded ? 'expand_circle_down' : 'more_horiz'}}</mat-icon>
</button>
}
</span>
}
`,
styles: [
`
.view-header {
display: flex;
flex-direction: column;
}
.right-btn-container {
display: flex;
align-items: center;
}
.right-btn-container .mat-mdc-slider {
min-width: 48px;
}
.right-btn-container .mdc-slider__input {
padding: 0px !important;
margin: 0 16px;
}
.icon-divider {
height: 50%;
}
.slider-container {
padding: 0 5px;
display: flex;
align-items: center;
}
.slider-icon {
min-width: 18px;
width: 18px;
height: 18px;
line-height: 18px;
font-size: 18px;
}
.filter-controls {
justify-content: space-between;
}
.block-filter-controls {
display: flex;
flex-direction: row;
align-items: baseline;
flex-shrink: 0;
}
.displays-section {
display: flex;
flex-direction: row;
align-items: center;
width: fit-content;
flex-wrap: nowrap;
}
.displays-select {
border-radius: 4px;
margin-left: 5px;
background-color: var(--disabled-color);
}
.rect-type-toggle {
margin: 0 4px;
}
.rects-content {
height: 100%;
display: flex;
flex-direction: column;
padding: 0px 12px;
}
.canvas-container {
height: 100%;
width: 100%;
position: relative;
}
.large-rects-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.large-rects-labels {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.mini-rects-canvas {
cursor: pointer;
width: 30%;
height: 30%;
top: 16px;
display: block;
position: absolute;
z-index: 1000;
}
.option-with-chip {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
}
.option-only-button {
padding: 0 10px;
border-radius: 10px;
background-color: var(--disabled-color) !important;
color: var(--default-text-color);
min-width: fit-content;
height: 18px;
align-items: center;
display: flex;
}
.rect-legend {
display: flex;
justify-content: space-between;
background-color: var(--card-title-background-color);
}
.shading-opts {
display: flex;
flex-wrap: wrap;
padding: 0 4px;
}
.shading-opts:not(.force-show-all) {
max-height: 24px;
overflow-y: hidden;
}
.shading-opt {
display: flex;
align-items: center;
padding: 2px;
}
.square {
width: 12px;
height: 12px;
line-height: 12px;
font-size: 12px;
border-style: solid;
border-width: 1.5px;
}
.shading-opt-desc {
padding-inline-start: 2px;
}
.rect-legend-expand-button {
height: 24px;
width: 24px;
padding: 0px;
}
`,
multlineTooltip,
iconDividerStyle,
viewerCardInnerStyle,
],
})
export class RectsComponent implements OnInit, OnDestroy {
Analytics = Analytics;
ViewerEvents = ViewerEvents;
@Input() title = 'title';
@Input() zoomFactor = 1;
@Input() store?: PersistentStore;
@Input() rects: UiRect[] = [];
@Input() miniRects: UiRect[] | undefined;
@Input() displays: DisplayIdentifier[] = [];
@Input() highlightedItem = '';
@Input() groupLabel = 'Displays';
@Input() isStackBased = false;
@Input() shadingModes: ShadingMode[] = [ShadingMode.GRADIENT];
@Input() rectSpec: RectSpec | undefined;
@Input() allRectSpecs: RectSpec[] | undefined;
@Input() userOptions: UserOptions = {};
@Input() dependencies: TraceType[] = [];
@Input() pinnedItems: UiHierarchyTreeNode[] = [];
@Input() isDarkMode = false;
@Output() collapseButtonClicked = new EventEmitter();
legendExpanded = false;
private internalRects: UiRect[] = [];
private internalMiniRects?: UiRect[];
private storeKeyZSpacingFactor = '';
private storeKeyShadingMode = '';
private storeKeySelectedDisplays = '';
private internalDisplays: DisplayIdentifier[] = [];
private internalHighlightedItem = '';
private currentDisplays: DisplayIdentifier[] = [];
largeRectsMapper3d = new Mapper3D();
private miniRectsMapper3d = new Mapper3D();
private largeRectsCanvas?: Canvas;
private miniRectsCanvas?: Canvas;
private resizeObserver = new ResizeObserver((entries) => {
this.updateLargeRectsPosition();
});
private largeRectsCanvasElement?: HTMLCanvasElement;
private miniRectsCanvasElement?: HTMLCanvasElement;
private largeRectsLabelsElement?: HTMLElement;
private mouseMoveListener = (event: MouseEvent) => this.onMouseMove(event);
private mouseUpListener = (event: MouseEvent) => this.onMouseUp(event);
private panning = false;
private defaultRectType: TraceRectType | undefined;
private static readonly ZOOM_SCROLL_RATIO = 0.3;
constructor(
@Inject(ElementRef) private elementRef: ElementRef<HTMLElement>,
@Inject(MatIconRegistry) private matIconRegistry: MatIconRegistry,
@Inject(DomSanitizer) private domSanitizer: DomSanitizer,
) {
this.matIconRegistry.addSvgIcon(
'cube_full_shade',
this.domSanitizer.bypassSecurityTrustResourceUrl(
getRootUrl() + 'cube_full_shade.svg',
),
);
this.matIconRegistry.addSvgIcon(
'cube_partial_shade',
this.domSanitizer.bypassSecurityTrustResourceUrl(
getRootUrl() + 'cube_partial_shade.svg',
),
);
}
ngOnInit() {
this.largeRectsMapper3d.setAllowedShadingModes(this.shadingModes);
const canvasContainer = assertDefined(
this.elementRef.nativeElement.querySelector<HTMLElement>(
'.rects-content',
),
);
this.resizeObserver.observe(canvasContainer);
this.largeRectsCanvasElement = assertDefined(
canvasContainer.querySelector<HTMLCanvasElement>('.large-rects-canvas'),
);
this.largeRectsLabelsElement = assertDefined(
canvasContainer.querySelector<HTMLElement>('.large-rects-labels'),
);
this.largeRectsCanvas = new Canvas(
this.largeRectsCanvasElement,
this.largeRectsLabelsElement,
() => this.isDarkMode,
);
this.largeRectsCanvasElement.addEventListener('mousedown', (event) =>
this.onCanvasMouseDown(event),
);
this.largeRectsMapper3d.increaseZoomFactor(this.zoomFactor - 1);
if (this.store) {
this.updateControlsFromStore();
}
this.redrawLargeRectsAndLabels();
this.miniRectsCanvasElement = canvasContainer.querySelector(
'.mini-rects-canvas',
)! as HTMLCanvasElement;
this.miniRectsCanvas = new Canvas(
this.miniRectsCanvasElement,
undefined,
() => this.isDarkMode,
);
this.miniRectsMapper3d.setShadingMode(ShadingMode.GRADIENT);
this.miniRectsMapper3d.resetToOrthogonalState();
if (this.miniRects && this.miniRects.length > 0) {
this.internalMiniRects = this.miniRects;
this.drawMiniRects();
}
this.defaultRectType = this.rectSpec?.type;
}
ngOnChanges(simpleChanges: SimpleChanges) {
this.handleLargeRectChanges(simpleChanges);
if (
simpleChanges['miniRects'] ||
(this.miniRects && simpleChanges['isDarkMode'])
) {
this.internalMiniRects = this.miniRects;
this.drawMiniRects();
}
}
private handleLargeRectChanges(simpleChanges: SimpleChanges) {
let displayChange = false;
if (simpleChanges['displays']) {
const curr: DisplayIdentifier[] = simpleChanges['displays'].currentValue;
const prev: DisplayIdentifier[] =
simpleChanges['displays'].previousValue ?? [];
displayChange =
curr.length !== prev.length ||
(curr.length > 0 &&
!curr.every((d, index) => d.displayId === prev[index].displayId));
}
let redrawRects = false;
let recolorRects = false;
let recolorLabels = false;
if (simpleChanges['pinnedItems']) {
this.largeRectsMapper3d.setPinnedItems(this.pinnedItems);
recolorRects = true;
}
if (simpleChanges['highlightedItem']) {
this.internalHighlightedItem =
simpleChanges['highlightedItem'].currentValue;
this.largeRectsMapper3d.setHighlightedRectId(
this.internalHighlightedItem,
);
recolorRects = true;
recolorLabels = true;
}
if (simpleChanges['isDarkMode']) {
recolorRects = true;
recolorLabels = true;
}
if (simpleChanges['rects']) {
this.internalRects = simpleChanges['rects'].currentValue;
redrawRects = true;
}
if (displayChange) {
this.onDisplaysChange(simpleChanges['displays']);
} else if (redrawRects) {
this.redrawLargeRectsAndLabels();
} else if (recolorRects && recolorLabels) {
this.updateLargeRectsAndLabelsColors();
} else if (recolorRects) {
this.updateLargeRectsColors();
}
}
ngOnDestroy() {
this.resizeObserver?.disconnect();
this.largeRectsCanvas?.onDestroy();
this.miniRectsCanvas?.onDestroy();
(this.largeRectsCanvasElement?.getContext('2d') as any)?.reset();
(this.miniRectsCanvasElement?.getContext('2d') as any)?.reset();
}
onDisplaysChange(change: SimpleChange) {
const displays = change.currentValue;
this.internalDisplays = displays;
const activeDisplay = this.getActiveDisplay(this.internalDisplays);
if (displays.length === 0) {
this.updateCurrentDisplays([], false);
return;
}
if (change.firstChange) {
this.updateCurrentDisplays([activeDisplay], false);
return;
}
const curr = this.internalDisplays.filter((display) =>
this.currentDisplays.some((curr) => curr.displayId === display.displayId),
);
if (curr.length > 0) {
this.updateCurrentDisplays(curr);
return;
}
const currGroupIds = this.largeRectsMapper3d.getCurrentGroupIds();
const displaysWithCurrentGroupId = this.internalDisplays.filter((display) =>
currGroupIds.some((curr) => curr === display.groupId),
);
if (displaysWithCurrentGroupId.length === 0) {
this.updateCurrentDisplays([activeDisplay]);
return;
}
this.updateCurrentDisplays([
this.getActiveDisplay(displaysWithCurrentGroupId),
]);
return;
}
updateControlsFromStore() {
this.storeKeyZSpacingFactor = `rectsView.${this.title}.zSpacingFactor`;
this.storeKeyShadingMode = `rectsView.${this.title}.shadingMode`;
this.storeKeySelectedDisplays = `rectsView.${this.title}.selectedDisplayId`;
const storedZSpacingFactor = assertDefined(this.store).get(
this.storeKeyZSpacingFactor,
);
if (storedZSpacingFactor !== undefined) {
this.largeRectsMapper3d.setZSpacingFactor(Number(storedZSpacingFactor));
}
const storedShadingMode = assertDefined(this.store).get(
this.storeKeyShadingMode,
);
if (
storedShadingMode !== undefined &&
this.shadingModes.includes(storedShadingMode as ShadingMode)
) {
this.largeRectsMapper3d.setShadingMode(storedShadingMode as ShadingMode);
}
const storedSelectedDisplays = assertDefined(this.store).get(
this.storeKeySelectedDisplays,
);
if (storedSelectedDisplays !== undefined) {
const storedIds: Array<number | string> = JSON.parse(
storedSelectedDisplays,
);
const displays = this.internalDisplays.filter((display) => {
return storedIds.some((id) => display.displayId === id);
});
if (displays.length > 0) {
this.currentDisplays = displays;
this.largeRectsMapper3d.setCurrentGroupIds(
displays.map((d) => d.groupId),
);
}
}
}
onSeparationSliderChange(factor: number) {
Analytics.Navigation.logRectSettingsChanged(
'z spacing',
factor,
TRACE_INFO[this.dependencies[0]].name,
);
this.store?.add(this.storeKeyZSpacingFactor, `${factor}`);
this.largeRectsMapper3d.setZSpacingFactor(factor);
this.redrawLargeRectsAndLabels();
}
onRotationSliderChange(factor: number) {
this.largeRectsMapper3d.setCameraRotationFactor(factor);
this.updateLargeRectsPositionAndLabels();
}
resetCamera() {
Analytics.Navigation.logZoom('reset', 'rects');
this.largeRectsMapper3d.resetCamera();
this.redrawLargeRectsAndLabels(true);
}
@HostListener('wheel', ['$event'])
onScroll(event: WheelEvent) {
if ((event.target as HTMLElement).className === 'large-rects-canvas') {
event.preventDefault();
if (event.deltaY > 0) {
Analytics.Navigation.logZoom('scroll', 'rects', 'out');
this.doZoomOut(RectsComponent.ZOOM_SCROLL_RATIO);
} else {
Analytics.Navigation.logZoom('scroll', 'rects', 'in');
this.doZoomIn(RectsComponent.ZOOM_SCROLL_RATIO);
}
}
}
onCanvasMouseDown(event: MouseEvent) {
document.addEventListener('mousemove', this.mouseMoveListener);
document.addEventListener('mouseup', this.mouseUpListener);
}
onMouseMove(event: MouseEvent) {
this.panning = true;
const distance: Distance = {dx: event.movementX, dy: event.movementY};
this.largeRectsMapper3d.addPanScreenDistance(distance);
this.updateLargeRectsPosition();
}
onMouseUp(event: MouseEvent) {
document.removeEventListener('mousemove', this.mouseMoveListener);
document.removeEventListener('mouseup', this.mouseUpListener);
}
onZoomInClick() {
Analytics.Navigation.logZoom('button', 'rects', 'in');
this.doZoomIn();
}
onZoomOutClick() {
Analytics.Navigation.logZoom('button', 'rects', 'out');
this.doZoomOut();
}
onDisplaySelectChange(event: MatSelectChange) {
const selectedDisplays: DisplayIdentifier[] = event.value;
this.updateCurrentDisplays(selectedDisplays);
}
getSelectTriggerValue(): string {
return this.currentDisplays.map((d) => d.name).join(', ');
}
onOnlyButtonClick(event: MouseEvent, selected: DisplayIdentifier) {
event.preventDefault();
event.stopPropagation();
this.updateCurrentDisplays([selected]);
}
onRectClick(event: MouseEvent) {
if (this.panning) {
this.panning = false;
return;
}
event.preventDefault();
const id = this.findClickedRectId(event);
if (id !== undefined) {
this.notifyHighlightedItem(id);
}
}
onRectDblClick(event: MouseEvent) {
event.preventDefault();
const clickedRectId = this.findClickedRectId(event);
if (clickedRectId === undefined) {
return;
}
this.elementRef.nativeElement.dispatchEvent(
new CustomEvent(ViewerEvents.RectsDblClick, {
bubbles: true,
detail: new RectDblClickDetail(clickedRectId),
}),
);
}
onMiniRectDblClick(event: MouseEvent) {
event.preventDefault();
this.elementRef.nativeElement.dispatchEvent(
new CustomEvent(ViewerEvents.MiniRectsDblClick, {bubbles: true}),
);
}
getZSpacingFactor(): number {
return this.largeRectsMapper3d.getZSpacingFactor();
}
getShadingMode(): ShadingMode {
return this.largeRectsMapper3d.getShadingMode();
}
onShadingModeButtonClicked() {
this.largeRectsMapper3d.updateShadingMode();
const newMode = this.largeRectsMapper3d.getShadingMode();
Analytics.Navigation.logRectSettingsChanged(
'shading mode',
newMode,
TRACE_INFO[this.dependencies[0]].name,
);
this.store?.add(this.storeKeyShadingMode, newMode);
this.updateLargeRectsColors();
}
onInteractionStart(components: CanColor[]) {
components.forEach((c) => (c.color = 'primary'));
}
onInteractionEnd(components: CanColor[]) {
components.forEach((c) => (c.color = 'accent'));
}
onRectTypeButtonClicked(event: MatButtonToggleChange) {
const spec: RectSpec = event.value;
this.elementRef.nativeElement.dispatchEvent(
new CustomEvent(ViewerEvents.RectTypeButtonClick, {
bubbles: true,
detail: {type: spec.type},
}),
);
}
showRectSpecWarning(): boolean {
return (
this.defaultRectType !== undefined &&
this.defaultRectType !== this.rectSpec?.type
);
}
showExpandButton(options: HTMLElement): boolean {
return (
options.scrollHeight > options.clientHeight ||
(this.legendExpanded && options.scrollHeight > 24)
);
}
private getActiveDisplay(displays: DisplayIdentifier[]): DisplayIdentifier {
const displaysWithRects = displays.filter((display) =>
this.internalRects.some(
(rect) => !rect.isDisplay && rect.groupId === display.groupId,
),
);
return (
displaysWithRects.find((display) => display.isActive) ??
displaysWithRects.at(0) ?? // fallback if no active displays
displays[0]
);
}
private updateCurrentDisplays(
displays: DisplayIdentifier[],
storeChange = true,
) {
if (storeChange) {
this.store?.add(
this.storeKeySelectedDisplays,
JSON.stringify(displays.map((d) => d.displayId)),
);
}
this.currentDisplays = displays;
this.largeRectsMapper3d.setCurrentGroupIds(displays.map((d) => d.groupId));
this.redrawLargeRectsAndLabels(true);
}
private findClickedRectId(event: MouseEvent): string | undefined {
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;
return this.largeRectsCanvas?.getClickedRectId(x, y);
}
private doZoomIn(ratio = 1) {
this.largeRectsMapper3d.increaseZoomFactor(ratio);
this.updateLargeRectsPositionAndLabels();
}
private doZoomOut(ratio = 1) {
this.largeRectsMapper3d.decreaseZoomFactor(ratio);
this.updateLargeRectsPositionAndLabels();
}
private redrawLargeRectsAndLabels(updateBoundingBox = false) {
this.largeRectsMapper3d.setRects(this.internalRects);
const scene = this.largeRectsMapper3d.computeScene(updateBoundingBox);
this.largeRectsCanvas?.updateViewPosition(
scene.camera,
scene.boundingBox,
scene.zDepth,
);
this.largeRectsCanvas?.updateRects(scene.rects);
this.largeRectsCanvas?.updateLabels(scene.labels);
this.largeRectsCanvas?.renderView();
}
private updateLargeRectsPosition() {
const scene = this.largeRectsMapper3d.computeScene(false);
this.largeRectsCanvas?.updateViewPosition(
scene.camera,
scene.boundingBox,
scene.zDepth,
);
this.largeRectsCanvas?.renderView();
}
private updateLargeRectsPositionAndLabels() {
const scene = this.largeRectsMapper3d.computeScene(false);
this.largeRectsCanvas?.updateViewPosition(
scene.camera,
scene.boundingBox,
scene.zDepth,
);
this.largeRectsCanvas?.updateLabels(scene.labels);
this.largeRectsCanvas?.renderView();
}
private updateLargeRectsColors() {
const scene = this.largeRectsMapper3d.computeScene(false);
this.largeRectsCanvas?.updateRects(scene.rects);
this.largeRectsCanvas?.renderView();
}
private updateLargeRectsAndLabelsColors() {
const scene = this.largeRectsMapper3d.computeScene(false);
this.largeRectsCanvas?.updateRects(scene.rects);
this.largeRectsCanvas?.updateLabels(scene.labels);
this.largeRectsCanvas?.renderView();
}
private drawMiniRects() {
if (this.internalMiniRects && this.miniRectsCanvas) {
this.miniRectsMapper3d.setShadingMode(ShadingMode.GRADIENT);
this.miniRectsMapper3d.setCurrentGroupIds([
this.internalMiniRects[0]?.groupId,
]);
this.miniRectsMapper3d.resetToOrthogonalState();
this.miniRectsMapper3d.setRects(this.internalMiniRects);
const scene = this.miniRectsMapper3d.computeScene(true);
this.miniRectsCanvas.updateViewPosition(
scene.camera,
scene.boundingBox,
scene.zDepth,
);
this.miniRectsCanvas.updateRects(scene.rects);
this.miniRectsCanvas.updateLabels(scene.labels);
this.miniRectsCanvas.renderView();
// Canvas internally sets these values to 100%. They need to be reset afterwards
if (this.miniRectsCanvasElement) {
this.miniRectsCanvasElement.style.width = '30%';
this.miniRectsCanvasElement.style.height = '30%';
}
}
}
private notifyHighlightedItem(id: string) {
const event: CustomEvent = new CustomEvent(
ViewerEvents.HighlightedIdChange,
{
bubbles: true,
detail: {id},
},
);
this.elementRef.nativeElement.dispatchEvent(event);
}
}