blob: 57e0aa67f0ba0129238ed911e953429143846089 [file] [log] [blame]
/*
* Copyright (C) 2024 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 {equal} from 'common/array_utils';
import {assertDefined} from 'common/assert_utils';
import {Box3D} from 'common/geometry/box3d';
import {CornerRadii} from 'common/geometry/corner_radii';
import {Point3D} from 'common/geometry/point3d';
import {TransformMatrix} from 'common/geometry/transform_matrix';
import {
getDefaultTransform,
TransformTypeFlags,
} from 'common/geometry/transform';
import * as THREE from 'three';
import {CSS2DObject} from 'three/examples/jsm/renderers/CSS2DRenderer';
import {ViewerEvents} from 'viewers/common/viewer_events';
import {Camera} from './camera';
import {Canvas} from './canvas';
import {ColorType} from './color_type';
import {RectLabel} from './rect_label';
import {UiRect3D} from './ui_rect3d';
describe('Canvas', () => {
const rectId = 'rect1';
describe('updateViewPosition', () => {
let canvasRects: HTMLCanvasElement;
let canvasLabels: HTMLElement;
let canvas: Canvas;
let canvasWidthSpy: jasmine.Spy;
let canvasHeightSpy: jasmine.Spy;
let camera: Camera;
let boundingBox: Box3D;
let graphicsScene: THREE.Scene;
let graphicsCamera: THREE.OrthographicCamera;
beforeEach(() => {
canvasRects = document.createElement('canvas');
canvasWidthSpy = spyOnProperty(
canvasRects,
'clientWidth',
).and.returnValue(100);
canvasHeightSpy = spyOnProperty(
canvasRects,
'clientHeight',
).and.returnValue(100);
canvasLabels = document.createElement('canvas');
canvas = new Canvas(canvasRects, canvasLabels);
camera = makeCamera();
boundingBox = makeBoundingBox();
[graphicsScene, graphicsCamera] = canvas.renderView();
});
it('handles zero size canvas element', () => {
const canvasRendererSetSizeSpy = spyOn(canvas.renderer, 'setSize');
canvasWidthSpy.and.returnValue(0);
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(canvasRendererSetSizeSpy).not.toHaveBeenCalled();
canvasWidthSpy.and.returnValue(100);
canvasHeightSpy.and.returnValue(0);
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(canvasRendererSetSizeSpy).not.toHaveBeenCalled();
});
it('changes camera lrtb and maintains scene translated position based on canvas aspect ratio', () => {
camera.panScreenDistance = {dx: 2, dy: 2};
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const [l, r, t, b] = [
graphicsCamera.left,
graphicsCamera.right,
graphicsCamera.top,
graphicsCamera.bottom,
];
const prevPosition = graphicsScene.position.clone();
canvasWidthSpy.and.returnValue(200);
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsCamera.left).toBeLessThan(l);
expect(graphicsCamera.right).toBeGreaterThan(r);
expect(graphicsCamera.top).toEqual(t);
expect(graphicsCamera.bottom).toEqual(b);
expect(graphicsScene.position).toEqual(prevPosition);
canvasWidthSpy.and.returnValue(100);
canvasHeightSpy.and.returnValue(200);
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsCamera.left).toEqual(l);
expect(graphicsCamera.right).toEqual(r);
expect(graphicsCamera.top).toBeGreaterThan(t);
expect(graphicsCamera.bottom).toBeLessThan(b);
expect(graphicsScene.position).toEqual(prevPosition);
});
it('changes scene translated position on change in pan screen distance', () => {
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevPosition = graphicsScene.position.clone();
const prevScale = graphicsScene.scale.clone();
camera.panScreenDistance = {dx: 2, dy: 2};
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.position.x).toBeGreaterThan(prevPosition.x);
expect(graphicsScene.position.y).toBeLessThan(prevPosition.y);
expect(graphicsScene.position.z).toEqual(prevPosition.z);
expect(graphicsScene.scale).toEqual(prevScale);
});
it('changes scene scale and scene translated position on change in zoom factor', () => {
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevPosition = graphicsScene.position.clone();
const sceneScale = graphicsScene.scale.clone();
camera.zoomFactor = 2;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.scale).toEqual(sceneScale.multiplyScalar(2));
expect(graphicsScene.position).toEqual(prevPosition.multiplyScalar(2));
});
it('changes camera position and scene translated x-position on change in rotation angle x', () => {
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevScenePos = graphicsScene.position.clone();
const prevCameraPos = graphicsCamera.position.clone();
camera.rotationAngleX = 1.5;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.position.x).toBeLessThan(prevScenePos.x);
expect(graphicsScene.position.y).toEqual(prevScenePos.y);
expect(graphicsScene.position.z).toEqual(prevScenePos.z);
expect(graphicsCamera.position.x).toEqual(prevCameraPos.x);
expect(graphicsCamera.position.y).toBeGreaterThan(prevCameraPos.y);
expect(graphicsCamera.position.z).toBeLessThan(prevCameraPos.z);
});
it('changes camera position and scene translated y-position on change in rotation angle y', () => {
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevScenePos = graphicsScene.position.clone();
const prevCameraPos = graphicsCamera.position.clone();
camera.rotationAngleY = 1.5;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.position.x).toEqual(prevScenePos.x);
expect(graphicsScene.position.y).toBeLessThan(prevScenePos.y);
expect(graphicsScene.position.z).toEqual(prevScenePos.z);
expect(graphicsCamera.position.x).toBeGreaterThan(prevCameraPos.x);
expect(graphicsCamera.position.y).toEqual(prevCameraPos.y);
expect(graphicsCamera.position.z).toBeLessThan(prevCameraPos.z);
});
it('changes scene scale and translated position on change in box diagonal', () => {
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevPosition = graphicsScene.position.clone();
const sceneScale = graphicsScene.scale.clone();
boundingBox.diagonal = 2;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.scale).toEqual(sceneScale.multiplyScalar(0.5));
expect(graphicsScene.position).toEqual(prevPosition.multiplyScalar(0.5));
});
it('changes translated position on change in box depth or zDepth', () => {
camera.rotationAngleX = 1;
camera.rotationAngleY = 1;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevPosition = graphicsScene.position.clone();
const prevScale = graphicsScene.scale.clone();
boundingBox.depth = 2;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.position).toEqual(prevPosition.multiplyScalar(2));
expect(graphicsScene.scale).toEqual(prevScale);
canvas.updateViewPosition(camera, boundingBox, 4);
expect(graphicsScene.position).toEqual(
prevPosition.multiply(new THREE.Vector3(1, 1, 2)),
);
expect(graphicsScene.scale).toEqual(prevScale);
});
it('changes translated position on change in box center', () => {
camera.rotationAngleX = 1;
camera.rotationAngleY = 1;
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
const prevPosition = graphicsScene.position.clone();
const prevScale = graphicsScene.scale.clone();
boundingBox.center = new Point3D(3, 3, 3);
canvas.updateViewPosition(camera, boundingBox, boundingBox.depth);
expect(graphicsScene.position).not.toEqual(prevPosition);
expect(graphicsScene.scale).toEqual(prevScale);
});
it('robust to no labels canvas', () => {
const canvas = new Canvas(canvasRects);
const box = makeBoundingBox();
canvas.updateViewPosition(makeCamera(), box, box.depth);
});
});
describe('updateRects', () => {
let canvas: Canvas;
let isDarkMode: boolean;
let graphicsScene: THREE.Scene;
beforeEach(() => {
isDarkMode = false;
const canvasRects = document.createElement('canvas');
const canvasLabels = document.createElement('canvas');
canvas = new Canvas(canvasRects, canvasLabels, () => isDarkMode);
graphicsScene = canvas.renderView()[0];
});
it('adds and removes rects', () => {
const mapDeleteSpy = spyOn(Map.prototype, 'delete').and.callThrough();
canvas.updateRects([]);
expect(graphicsScene.getObjectByName(rectId)).toBeUndefined();
canvas.updateRects([makeUiRect3D(rectId)]);
expect(graphicsScene.getObjectByName(rectId)).toBeDefined();
canvas.updateRects([]);
expect(graphicsScene.getObjectByName(rectId)).toBeUndefined();
expect(mapDeleteSpy).toHaveBeenCalledOnceWith(rectId);
});
it('updates existing rects instead of adding new rect', () => {
const rect = makeUiRect3D(rectId);
canvas.updateRects([rect]);
const rectMesh = getRectMesh(rectId);
expect(rectMesh.position.z).toBe(0);
const newRect = makeUiRect3D(rectId);
newRect.topLeft = new Point3D(0, 0, 1);
canvas.updateRects([newRect]);
expect(rectMesh.position.z).toBe(1);
expect(getRectMesh('rect1')).toEqual(rectMesh);
});
it('makes rect with correct position and borders', () => {
const rect = makeUiRect3D(rectId);
rect.topLeft = new Point3D(1, 1, 5);
rect.bottomRight = new Point3D(2, 2, 5);
canvas.updateRects([rect]);
const rectMesh = getRectMesh(rectId);
expect(rectMesh.position.z).toBe(5);
checkBorderColor(rectId, Canvas.RECT_EDGE_COLOR_LIGHT_MODE);
isDarkMode = true;
canvas.updateRects([rect]);
checkBorderColor(rectId, Canvas.RECT_EDGE_COLOR_DARK_MODE);
});
it('makes rect with correct fill material', () => {
const rect = makeUiRect3D(rectId);
canvas.updateRects([rect]);
const rectMesh = getRectMesh(rectId);
const defaultVisibleRectColor = new THREE.Color(0xc8e8b7);
checkMaterialColorAndOpacity(
rectMesh,
defaultVisibleRectColor,
Canvas.OPACITY_REGULAR,
);
const visibleWithOpacity = makeUiRect3D(rectId);
visibleWithOpacity.colorType = ColorType.VISIBLE_WITH_OPACITY;
canvas.updateRects([visibleWithOpacity]);
const material = rectMesh.material as THREE.MeshBasicMaterial;
expect(material.color).not.toEqual(defaultVisibleRectColor);
expect(material.opacity).toBe(1);
const nonVisible = makeUiRect3D(rectId);
nonVisible.colorType = ColorType.NOT_VISIBLE;
canvas.updateRects([nonVisible]);
checkMaterialColorAndOpacity(
rectMesh,
new THREE.Color(0xdcdcdc),
Canvas.OPACITY_REGULAR,
);
const highlighted = makeUiRect3D(rectId);
highlighted.colorType = ColorType.HIGHLIGHTED;
canvas.updateRects([highlighted]);
checkMaterialColorAndOpacity(
rectMesh,
Canvas.RECT_COLOR_HIGHLIGHTED_LIGHT_MODE,
Canvas.OPACITY_REGULAR,
);
isDarkMode = true;
canvas.updateRects([highlighted]);
checkMaterialColorAndOpacity(
rectMesh,
Canvas.RECT_COLOR_HIGHLIGHTED_DARK_MODE,
Canvas.OPACITY_REGULAR,
);
isDarkMode = false;
const highlightedWithOpacity = makeUiRect3D(rectId);
highlightedWithOpacity.colorType = ColorType.HIGHLIGHTED_WITH_OPACITY;
canvas.updateRects([highlightedWithOpacity]);
checkMaterialColorAndOpacity(
rectMesh,
Canvas.RECT_COLOR_HIGHLIGHTED_LIGHT_MODE,
highlightedWithOpacity.darkFactor,
);
isDarkMode = true;
canvas.updateRects([highlightedWithOpacity]);
checkMaterialColorAndOpacity(
rectMesh,
Canvas.RECT_COLOR_HIGHLIGHTED_DARK_MODE,
highlightedWithOpacity.darkFactor,
);
const contentAndOpacity = makeUiRect3D(rectId);
contentAndOpacity.colorType = ColorType.HAS_CONTENT_AND_OPACITY;
canvas.updateRects([contentAndOpacity]);
checkMaterialColorAndOpacity(rectMesh, Canvas.RECT_COLOR_HAS_CONTENT, 1);
const content = makeUiRect3D(rectId);
content.colorType = ColorType.HAS_CONTENT;
canvas.updateRects([content]);
checkMaterialColorAndOpacity(
rectMesh,
Canvas.RECT_COLOR_HAS_CONTENT,
Canvas.OPACITY_REGULAR,
);
const oversized = makeUiRect3D(rectId);
oversized.colorType = ColorType.HAS_CONTENT;
oversized.isOversized = true;
canvas.updateRects([oversized]);
checkMaterialColorAndOpacity(
rectMesh,
Canvas.RECT_COLOR_HAS_CONTENT,
Canvas.OPACITY_OVERSIZED,
);
const empty = makeUiRect3D(rectId);
empty.colorType = ColorType.EMPTY;
canvas.updateRects([empty]);
expect(rectMesh.material).toEqual(Canvas.TRANSPARENT_MATERIAL);
});
it('makes rect with fill region', () => {
const rect = makeUiRect3D(rectId);
rect.fillRegion = [];
rect.colorType = ColorType.HAS_CONTENT;
canvas.updateRects([rect]);
const rectMesh = getRectMesh(rectId);
expect(rectMesh.material).toEqual(Canvas.TRANSPARENT_MATERIAL);
const fillRegionMesh = getFillRegionMesh(rectId);
expect(fillRegionMesh.position.z).toBe(1);
checkMaterialColorAndOpacity(
fillRegionMesh,
Canvas.RECT_COLOR_HAS_CONTENT,
Canvas.OPACITY_REGULAR,
);
});
it('makes rect with pinned borders', () => {
const rect = makeUiRect3D(rectId);
rect.topLeft = new Point3D(1, 1, 5);
rect.bottomRight = new Point3D(2, 2, 5);
rect.isPinned = true;
const rect2 = makeUiRect3D('rect2');
rect2.topLeft = new Point3D(1, 1, 5);
rect2.bottomRight = new Point3D(2, 2, 5);
rect2.isPinned = true;
canvas.updateRects([rect, rect2]);
checkBorderColor(rect.id, Canvas.RECT_EDGE_COLOR_PINNED);
checkBorderColor(rect2.id, Canvas.RECT_EDGE_COLOR_PINNED_ALT);
});
it('handles changes in geometry', () => {
const rect = makeUiRect3D(rectId);
canvas.updateRects([rect]);
const rectMesh = getRectMesh(rectId);
let rectGeometryId = rectMesh.geometry.id;
// no change
canvas.updateRects([rect]);
expect(rectMesh.geometry.id).toEqual(rectGeometryId);
// geometry object replaced
const roundRect = makeUiRect3D(rectId);
roundRect.cornerRadii = new CornerRadii(0, 0.4, 0.3, 0.2);
updateRectsAndCheckGeometryId(roundRect, rectMesh, rectGeometryId);
rectGeometryId = rectMesh.geometry.id;
const diffRadii = makeUiRect3D(rectId);
diffRadii.cornerRadii = new CornerRadii(0.5, 0.4, 0.3, 0.2);
updateRectsAndCheckGeometryId(diffRadii, rectMesh, rectGeometryId);
rectGeometryId = rectMesh.geometry.id;
const bottomRightChanged = makeUiRect3D(rectId);
bottomRightChanged.cornerRadii = new CornerRadii(0.5, 0.4, 0.3, 0.2);
bottomRightChanged.bottomRight = new Point3D(5, 5, 5);
updateRectsAndCheckGeometryId(
bottomRightChanged,
rectMesh,
rectGeometryId,
);
rectGeometryId = rectMesh.geometry.id;
const topLeftChanged = makeUiRect3D(rectId);
topLeftChanged.cornerRadii = new CornerRadii(0.5, 0.4, 0.3, 0.2);
topLeftChanged.bottomRight = new Point3D(5, 5, 5);
topLeftChanged.topLeft = new Point3D(0, 0, 5);
updateRectsAndCheckGeometryId(topLeftChanged, rectMesh, rectGeometryId);
rectGeometryId = rectMesh.geometry.id;
const noRadii = makeUiRect3D(rectId);
noRadii.bottomRight = new Point3D(5, 5, 5);
noRadii.topLeft = new Point3D(0, 0, 5);
updateRectsAndCheckGeometryId(noRadii, rectMesh, rectGeometryId);
const prevRectMeshId = rectMesh.id;
const rotated = makeUiRect3D(rectId);
rotated.bottomRight = new Point3D(5, 5, 5);
rotated.topLeft = new Point3D(0, 0, 5);
rotated.transform = getDefaultTransform(
TransformTypeFlags.ROT_90_VAL,
2,
2,
).matrix;
canvas.updateRects([rotated]);
expect(getRectMesh(rectId).id).not.toEqual(prevRectMeshId);
});
it('handles changes in fill region', () => {
const noFillRegion = makeUiRect3D(rectId);
canvas.updateRects([noFillRegion]);
const rectMesh = getRectMesh(rectId);
expect(
rectMesh.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.fillRegion),
).toBeUndefined();
expect(
(rectMesh.material as THREE.MeshBasicMaterial).color.getHex(),
).toBe(13166775);
const emptyFillRegion = makeUiRect3D(rectId);
emptyFillRegion.fillRegion = [];
canvas.updateRects([emptyFillRegion]);
const fillRegionMesh = getFillRegionMesh(rectId);
expect(rectMesh.material).toEqual(Canvas.TRANSPARENT_MATERIAL);
expect(
(fillRegionMesh.material as THREE.MeshBasicMaterial).color.getHex(),
).toBe(13166775);
let fillRegionGeometryId = fillRegionMesh.geometry.id;
const emptyFillRegionWithContent = makeUiRect3D(rectId);
emptyFillRegionWithContent.fillRegion = [];
emptyFillRegionWithContent.colorType = ColorType.HAS_CONTENT;
canvas.updateRects([emptyFillRegionWithContent]);
expect(rectMesh.material).toEqual(Canvas.TRANSPARENT_MATERIAL);
checkMaterialColorAndOpacity(
fillRegionMesh,
Canvas.RECT_COLOR_HAS_CONTENT,
Canvas.OPACITY_REGULAR,
);
let newGeometry = getFillRegionMesh(rectId).geometry;
expect(newGeometry.id).toEqual(fillRegionGeometryId);
const validFillRegion = makeUiRect3D(rectId);
validFillRegion.fillRegion = [
{
topLeft: emptyFillRegion.topLeft,
bottomRight: emptyFillRegion.bottomRight,
},
];
canvas.updateRects([validFillRegion]);
newGeometry = getFillRegionMesh(rectId).geometry;
expect(newGeometry.id).not.toEqual(fillRegionGeometryId);
fillRegionGeometryId = newGeometry.id;
const differentFillRegion = makeUiRect3D(rectId);
differentFillRegion.fillRegion = [
{
topLeft: validFillRegion.fillRegion[0].topLeft,
bottomRight: new Point3D(4, 4, 2),
},
];
canvas.updateRects([differentFillRegion]);
newGeometry = getFillRegionMesh(rectId).geometry;
expect(newGeometry.id).not.toEqual(fillRegionGeometryId);
fillRegionGeometryId = newGeometry.id;
canvas.updateRects([noFillRegion]);
expect(
getRectMesh(rectId).getObjectByName(
rectId + Canvas.GRAPHICS_NAMES.fillRegion,
),
).toBeUndefined();
});
it('handles change from normal to pinned borders', () => {
const rect = makeUiRect3D(rectId);
rect.topLeft = new Point3D(1, 1, 5);
rect.bottomRight = new Point3D(2, 2, 5);
canvas.updateRects([rect]);
checkBorderColor(rect.id, Canvas.RECT_EDGE_COLOR_LIGHT_MODE);
const pinnedRect = makeUiRect3D(rectId);
pinnedRect.topLeft = new Point3D(1, 1, 5);
pinnedRect.bottomRight = new Point3D(2, 2, 5);
pinnedRect.isPinned = true;
canvas.updateRects([pinnedRect]);
checkBorderColor(rect.id, Canvas.RECT_EDGE_COLOR_PINNED);
});
it('adds pointers', () => {
const rect = makeUiRect3D(rectId);
rect.pointerLocationsInRect = [
new Point3D(2, 2, 2),
new Point3D(4, 4, 4),
];
canvas.updateRects([rect]);
checkNextPointer(rect.pointerLocationsInRect[0], new Point3D(2, 2, 3));
checkNextPointer(new Point3D(4, 4, 2), new Point3D(4, 4, 3));
});
it('handles changes in number of pointers', () => {
const rect = makeUiRect3D(rectId);
rect.pointerLocationsInRect = [new Point3D(2, 2, 2)];
canvas.updateRects([rect]);
const circleId = getPointerCircle(rectId).id;
const crosshairsId = getPointerCrosshairs(rectId).id;
const rect2 = makeUiRect3D(rectId);
rect2.pointerLocationsInRect = [new Point3D(2, 2, 2)];
canvas.updateRects([rect2]);
expect(getPointerCircle(rectId).id).toBe(circleId);
expect(getPointerCrosshairs(rectId).id).toBe(crosshairsId);
expect(countPointers(rectId)).toBe(1);
const rect3 = makeUiRect3D(rectId);
rect3.pointerLocationsInRect = [
new Point3D(2, 2, 2),
new Point3D(1, 2, 2),
];
canvas.updateRects([rect3]);
expect(getPointerCircle(rectId).id).not.toBe(circleId);
expect(getPointerCrosshairs(rectId).id).not.toBe(crosshairsId);
expect(countPointers(rectId)).toBe(2);
const rect4 = makeUiRect3D(rectId);
rect4.pointerLocationsInRect = [new Point3D(1, 2, 2)];
canvas.updateRects([rect4]);
const newCircle = getPointerCircle(rectId);
expect(newCircle.id).not.toBe(circleId);
expect(getPointerCrosshairs(rectId).id).not.toBe(crosshairsId);
expect(countPointers(rectId)).toBe(1);
checkVectorEqualToPoint(newCircle.position, new Point3D(1, 2, 2));
});
it('changes pointer thickness', () => {
const rect = makeUiRect3D(rectId);
rect.pointerLocationsInRect = [new Point3D(2, 2, 2)];
canvas.updateRects([rect]);
const crosshairs = getPointerCrosshairs(rectId);
checkGeometryPosition(crosshairs, [-40, -1.5, 0, -40, 1.5, 0]);
const rect2 = makeUiRect3D(rectId);
rect2.colorType = ColorType.HIGHLIGHTED;
rect2.pointerLocationsInRect = [new Point3D(2, 2, 2)];
canvas.updateRects([rect2]);
const crosshairs2 = getPointerCrosshairs(rectId);
checkGeometryPosition(crosshairs2, [-40, -5.5, 0, -40, 5.5, 0]);
const rect3 = makeUiRect3D(rectId);
rect3.colorType = ColorType.HIGHLIGHTED_WITH_OPACITY;
rect3.pointerLocationsInRect = [new Point3D(2, 2, 2)];
canvas.updateRects([rect3]);
const crosshairs3 = getPointerCrosshairs(rectId);
checkGeometryPosition(crosshairs3, [-40, -5.5, 0, -40, 5.5, 0]);
});
it('adds rays', () => {
const rect = makeUiRect3D(rectId);
rect.rayLocationsInScene = [new Point3D(2, 2, 2), new Point3D(4, 4, 4)];
canvas.updateRects([rect]);
checkNextRay(rect.rayLocationsInScene[0]);
checkNextRay(rect.rayLocationsInScene[1]);
});
it('handles changes in number of rays', () => {
const rect = makeUiRect3D(rectId);
rect.rayLocationsInScene = [new Point3D(2, 3, 4)];
canvas.updateRects([rect]);
const rayId = getRayLine(rectId).id;
const rayName = rectId + Canvas.GRAPHICS_NAMES.ray;
const rect2 = makeUiRect3D(rectId);
rect2.rayLocationsInScene = [new Point3D(2, 3, 4)];
canvas.updateRects([rect2]);
expect(getRayLine(rectId).id).toBe(rayId);
expect(countObject(rayName, graphicsScene)).toBe(1);
const rect3 = makeUiRect3D(rectId);
rect3.rayLocationsInScene = [new Point3D(2, 3, 4), new Point3D(4, 4, 4)];
canvas.updateRects([rect3]);
expect(getRayLine(rectId).id).not.toBe(rayId);
expect(countObject(rayName, graphicsScene)).toBe(2);
const rect4 = makeUiRect3D(rectId);
rect4.rayLocationsInScene = [new Point3D(4, 4, 4)];
canvas.updateRects([rect4]);
const newRay = getRayLine(rectId);
expect(newRay.id).not.toBe(rayId);
expect(countObject(rayName, graphicsScene)).toBe(1);
checkVectorEqualToPoint(newRay.position, rect4.rayLocationsInScene[0]);
});
function checkMaterialColorAndOpacity(
mesh: THREE.Mesh | THREE.Line,
color: THREE.Color | number,
opacity: number,
) {
const material = mesh.material as THREE.MeshBasicMaterial;
expect(
color instanceof THREE.Color ? material.color : material.color.getHex(),
).toEqual(color);
expect(material.opacity).toEqual(opacity);
}
function checkBorderColor(id: string, color: THREE.Color | number) {
const rectMesh = getRectMesh(id);
const border = getBorders(id + Canvas.GRAPHICS_NAMES.border, rectMesh);
const meshColor = border.material.color;
expect(
color instanceof THREE.Color ? meshColor : meshColor.getHex(),
).toEqual(color);
}
function getBorders(id: string, root: THREE.Object3D) {
const segments = assertDefined(root.getObjectByName(id));
return segments as THREE.LineSegments<
THREE.EdgesGeometry,
THREE.LineBasicMaterial
>;
}
function getRectMesh(id: string): THREE.Mesh {
return getMesh(id, graphicsScene);
}
function getFillRegionMesh(id: string): THREE.Mesh {
const rectMesh = getRectMesh(id);
return getMesh(id + Canvas.GRAPHICS_NAMES.fillRegion, rectMesh);
}
function getPointerCircle(id: string): THREE.Mesh {
const rectMesh = getRectMesh(id);
return getMesh(rectId + Canvas.GRAPHICS_NAMES.pointerCircle, rectMesh);
}
function getPointerCrosshairs(id: string): THREE.Mesh {
const rectMesh = getRectMesh(id);
return getMesh(
rectId + Canvas.GRAPHICS_NAMES.pointerCrosshairs,
rectMesh,
);
}
function checkNextPointer(expCircle: Point3D, expCrosshairs: Point3D) {
const expectedColor = Canvas.RECT_EDGE_COLOR_LIGHT_MODE;
const circle = getPointerCircle(rectId);
expect((circle.geometry as THREE.CircleGeometry).parameters.radius).toBe(
10,
);
checkVectorEqualToPoint(circle.position, expCircle);
checkMaterialColorAndOpacity(circle, expectedColor, 1);
const crosshairs = getPointerCrosshairs(rectId);
checkVectorEqualToPoint(crosshairs.position, expCrosshairs);
checkMaterialColorAndOpacity(circle, expectedColor, 1);
// change names so next objects can be retrieved from mesh
circle.name = 'circle';
crosshairs.name = 'crosshairs';
}
function checkNextRay(expectedPosition: Point3D) {
const ray = getRayLine(rectId);
checkGeometryPosition(ray, [0, 0, 0, 0, 0, 1500]);
checkVectorEqualToPoint(ray.position, expectedPosition);
checkMaterialColorAndOpacity(ray, Canvas.RECT_EDGE_COLOR_LIGHT_MODE, 1);
ray.name = 'ray'; // change name so next can be retrieved from scene
}
function countPointers(id: string) {
const rectMesh = getRectMesh(id);
const circleCount = countObject(
id + Canvas.GRAPHICS_NAMES.pointerCircle,
rectMesh,
);
const crosshairsCount = countObject(
id + Canvas.GRAPHICS_NAMES.pointerCrosshairs,
rectMesh,
);
expect(circleCount).toEqual(crosshairsCount);
return circleCount;
}
function countObject(name: string, root: THREE.Object3D) {
let obj = root.getObjectByName(name);
let count = 0;
while (obj) {
count++;
obj.name = 'obj';
obj = root.getObjectByName(name);
}
return count;
}
function getRayLine(id: string): THREE.Line {
return getLine(id + Canvas.GRAPHICS_NAMES.ray, graphicsScene);
}
function checkGeometryPosition(
obj: THREE.Mesh | THREE.Line,
expected: number[],
) {
expect(
equal(
Array.from(obj.geometry.getAttribute('position').array).slice(
0,
expected.length,
),
expected,
),
).toBeTrue();
}
function updateRectsAndCheckGeometryId(
rect: UiRect3D,
rectMesh: THREE.Mesh,
prevId: number,
) {
canvas.updateRects([rect]);
expect(rectMesh.geometry.id).not.toEqual(prevId);
}
});
describe('updateLabels', () => {
let canvas: Canvas;
let isDarkMode: boolean;
let graphicsScene: THREE.Scene;
beforeEach(() => {
isDarkMode = false;
const canvasRects = document.createElement('canvas');
const canvasLabels = document.createElement('canvas');
canvas = new Canvas(canvasRects, canvasLabels, () => isDarkMode);
graphicsScene = canvas.renderView()[0];
});
it('adds and removes labels', () => {
const mapDeleteSpy = spyOn(Map.prototype, 'delete').and.callThrough();
canvas.updateLabels([]);
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.circle),
).toBeUndefined();
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.line),
).toBeUndefined();
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.text),
).toBeUndefined();
canvas.updateLabels([makeRectLabel(rectId)]);
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.circle),
).toBeDefined();
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.line),
).toBeDefined();
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.text),
).toBeDefined();
canvas.updateLabels([]);
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.circle),
).toBeUndefined();
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.line),
).toBeUndefined();
expect(
graphicsScene.getObjectByName(rectId + Canvas.GRAPHICS_NAMES.text),
).toBeUndefined();
expect(mapDeleteSpy).toHaveBeenCalledOnceWith(rectId);
});
it('updates existing labels instead of adding new labels', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const circleMesh = getCircleMesh(rectId);
const geometryId = circleMesh.geometry.id;
const newLabel = makeRectLabel(rectId);
newLabel.circle.radius = 2;
canvas.updateLabels([newLabel]);
expect(getCircleMesh(rectId)).toEqual(circleMesh);
expect(circleMesh.geometry.id).not.toEqual(geometryId);
});
it('makes label with correct circle and text geometry', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const circleMesh = getCircleMesh(rectId);
expect(
(circleMesh.geometry as THREE.CircleGeometry).parameters.radius,
).toEqual(label.circle.radius);
checkVectorEqualToPoint(circleMesh.position, label.circle.center);
const text = getText(rectId);
checkVectorEqualToPoint(text.position, label.textCenter);
});
it('handles change in circle radius', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const circleMesh = getCircleMesh(rectId);
const newLabel = makeRectLabel(rectId);
newLabel.circle.radius = 2;
canvas.updateLabels([newLabel]);
expect(
(circleMesh.geometry as THREE.CircleGeometry).parameters.radius,
).toBe(2);
});
it('handles change in circle center', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const circleMesh = getCircleMesh(rectId);
const newLabel = makeRectLabel(rectId);
newLabel.circle.center = new Point3D(1, 1, 1);
canvas.updateLabels([newLabel]);
checkVectorEqualToPoint(circleMesh.position, newLabel.circle.center);
});
it('applies colors based on highlighted or dark mode state', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const circleMesh = getCircleMesh(rectId);
const line = getLabelLine(rectId);
const text = getText(rectId);
expect(
(circleMesh.material as THREE.LineBasicMaterial).color.getHex(),
).toEqual(Canvas.LABEL_LINE_COLOR);
expect((line.material as THREE.LineBasicMaterial).color.getHex()).toEqual(
Canvas.LABEL_LINE_COLOR,
);
expect(text.element.style.color).toBe('gray');
const highlighted = makeRectLabel(rectId);
highlighted.isHighlighted = true;
canvas.updateLabels([highlighted]);
expect(
(circleMesh.material as THREE.LineBasicMaterial).color.getHex(),
).toEqual(Canvas.RECT_EDGE_COLOR_LIGHT_MODE);
expect((line.material as THREE.LineBasicMaterial).color.getHex()).toEqual(
Canvas.RECT_EDGE_COLOR_LIGHT_MODE,
);
expect(text.element.style.color).toBe('');
isDarkMode = true;
canvas.updateLabels([highlighted]);
expect(
(circleMesh.material as THREE.LineBasicMaterial).color.getHex(),
).toEqual(Canvas.RECT_EDGE_COLOR_DARK_MODE);
expect((line.material as THREE.LineBasicMaterial).color.getHex()).toEqual(
Canvas.RECT_EDGE_COLOR_DARK_MODE,
);
expect(text.element.style.color).toBe('');
canvas.updateLabels([label]);
expect(
(circleMesh.material as THREE.LineBasicMaterial).color.getHex(),
).toEqual(Canvas.LABEL_LINE_COLOR);
expect((line.material as THREE.LineBasicMaterial).color.getHex()).toEqual(
Canvas.LABEL_LINE_COLOR,
);
expect(text.element.style.color).toBe('gray');
});
it('handles change in line points', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const line = getLabelLine(rectId);
const geometryId = line.geometry.id;
const newLabel = makeRectLabel(rectId);
newLabel.linePoints = [new Point3D(1, 1, 1), new Point3D(1, 2, 1)];
canvas.updateLabels([newLabel]);
expect(line.geometry.id).not.toEqual(geometryId);
});
it('handles change in text center', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const text = getText(rectId);
const newLabel = makeRectLabel(rectId);
newLabel.textCenter = new Point3D(1, 15, 1);
canvas.updateLabels([newLabel]);
checkVectorEqualToPoint(text.position, newLabel.textCenter);
});
it('robust to no labels canvas', () => {
const canvasRects = document.createElement('canvas');
const canvas = new Canvas(canvasRects);
canvas.updateLabels([]);
});
it('propagates highlighted item on text click', () => {
const label = makeRectLabel(rectId);
canvas.updateLabels([label]);
const text = getText(rectId);
let id: string | undefined;
text.element.addEventListener(
ViewerEvents.HighlightedIdChange,
(event) => {
id = (event as CustomEvent).detail.id;
},
);
text.element.click();
expect(id).toEqual(rectId);
});
function getCircleMesh(id: string): THREE.Mesh {
return getMesh(id + Canvas.GRAPHICS_NAMES.circle, graphicsScene);
}
function getLabelLine(id: string): THREE.Line {
return getLine(id + Canvas.GRAPHICS_NAMES.line, graphicsScene);
}
function getText(id: string): CSS2DObject {
return assertDefined(
graphicsScene.getObjectByName(id + Canvas.GRAPHICS_NAMES.text),
) as CSS2DObject;
}
});
describe('renderView', () => {
let canvas: Canvas;
let rectsCompileSpy: jasmine.Spy;
let renderingSpies: jasmine.Spy[];
beforeEach(() => {
const canvasRects = document.createElement('canvas');
const canvasLabels = document.createElement('canvas');
canvas = new Canvas(canvasRects, canvasLabels);
rectsCompileSpy = spyOn(assertDefined(canvas.renderer), 'compile');
renderingSpies = [
spyOn(assertDefined(canvas.renderer), 'setPixelRatio'),
spyOn(assertDefined(canvas.renderer), 'render'),
spyOn(assertDefined(canvas.labelRenderer), 'render'),
];
});
it('sets pixel ratio and renders rects and labels', () => {
canvas.renderView();
checkRenderSpiesCalled(1);
});
it('only compiles on first call', () => {
canvas.renderView();
expect(rectsCompileSpy).toHaveBeenCalledTimes(1);
checkRenderSpiesCalled(1);
canvas.renderView();
expect(rectsCompileSpy).toHaveBeenCalledTimes(1);
checkRenderSpiesCalled(2);
});
it('robust to no labels canvas', () => {
const canvasRects = document.createElement('canvas');
const canvas = new Canvas(canvasRects);
canvas.renderView();
});
function checkRenderSpiesCalled(times: number) {
renderingSpies.forEach((spy) => expect(spy).toHaveBeenCalledTimes(times));
}
});
describe('getClickedRectId', () => {
let canvas: Canvas;
beforeEach(() => {
const canvasRects = document.createElement('canvas');
const canvasLabels = document.createElement('canvas');
canvas = new Canvas(canvasRects, canvasLabels);
const box = makeBoundingBox();
canvas.updateViewPosition(makeCamera(), box, box.depth);
canvas.renderView();
});
it('identifies clicked rect', () => {
const rect = makeUiRect3D(rectId);
rect.isClickable = true;
canvas.updateRects([rect]);
canvas.renderView();
const id = canvas.getClickedRectId(0.1, 0.1);
expect(id).toBe('rect1');
});
it('identifies clicked rect from fill region', () => {
const rect = makeUiRect3D(rectId);
rect.fillRegion = [
{topLeft: rect.topLeft, bottomRight: rect.bottomRight},
];
rect.isClickable = true;
canvas.updateRects([rect]);
canvas.renderView();
const id = canvas.getClickedRectId(0.1, 0.1);
expect(id).toBe('rect1');
});
it('does not identify rect if not clickable', () => {
const rect = makeUiRect3D(rectId);
canvas.updateRects([rect]);
expect(canvas.getClickedRectId(0.1, 0.1)).toBeUndefined();
});
it('does not identify rect out of click area', () => {
const rect = makeUiRect3D(rectId);
rect.isClickable = true;
canvas.updateRects([rect]);
expect(canvas.getClickedRectId(2, 2)).toBeUndefined();
});
});
function makeCamera(): Camera {
return {
rotationAngleX: 0,
rotationAngleY: 0,
zoomFactor: 1,
panScreenDistance: {dx: 0, dy: 0},
};
}
function makeBoundingBox(): Box3D {
return {
width: 1,
height: 1,
depth: 1,
center: new Point3D(0, 0, 0),
diagonal: 1,
};
}
function makeUiRect3D(id: string): UiRect3D {
return {
id,
topLeft: new Point3D(0, 0, 0),
bottomRight: new Point3D(1, 1, 0),
cornerRadii: undefined,
darkFactor: 1,
colorType: ColorType.VISIBLE,
isClickable: false,
transform: TransformMatrix.IDENTITY,
isOversized: false,
fillRegion: undefined,
isPinned: false,
pointerLocationsInRect: [],
rayLocationsInScene: [],
};
}
function makeRectLabel(id: string): RectLabel {
return {
circle: {radius: 1, center: new Point3D(0, 0, 0)},
linePoints: [new Point3D(0, 0, 0), new Point3D(0, 1, 0)],
textCenter: new Point3D(0, 12, 0),
text: id,
isHighlighted: false,
rectId: id,
};
}
function getMesh(id: string, root: THREE.Object3D): THREE.Mesh {
const mesh = assertDefined(root.getObjectByName(id));
expect(mesh).toBeInstanceOf(THREE.Mesh);
return mesh as THREE.Mesh;
}
function getLine(id: string, root: THREE.Object3D): THREE.Line {
const mesh = assertDefined(root.getObjectByName(id));
expect(mesh).toBeInstanceOf(THREE.Line);
return mesh as THREE.Line;
}
function checkVectorEqualToPoint(vector: THREE.Vector3, point: Point3D) {
expect(
vector.equals(new THREE.Vector3(point.x, point.y, point.z)),
).toBeTrue();
}
});