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 */ 16 17import {Rectangle, Size} from 'viewers/common/rectangle'; 18import { 19 Box3D, 20 ColorType, 21 Distance2D, 22 Label3D, 23 Point3D, 24 Rect3D, 25 Scene3D, 26 Transform3D, 27} from './types3d'; 28 29class Mapper3D { 30 private static readonly CAMERA_ROTATION_FACTOR_INIT = 1; 31 private static readonly Z_SPACING_FACTOR_INIT = 1; 32 private static readonly Z_SPACING_MAX = 200; 33 private static readonly LABEL_FIRST_Y_OFFSET = 100; 34 private static readonly LABEL_TEXT_Y_SPACING = 200; 35 private static readonly LABEL_CIRCLE_RADIUS = 15; 36 private static readonly ZOOM_FACTOR_INIT = 1; 37 private static readonly ZOOM_FACTOR_MIN = 0.1; 38 private static readonly ZOOM_FACTOR_MAX = 8.5; 39 private static readonly ZOOM_FACTOR_STEP = 0.2; 40 private static readonly IDENTITY_TRANSFORM: Transform3D = { 41 dsdx: 1, 42 dsdy: 0, 43 tx: 0, 44 dtdx: 0, 45 dtdy: 1, 46 ty: 0, 47 }; 48 49 private rects: Rectangle[] = []; 50 private highlightedRectIds: string[] = []; 51 private cameraRotationFactor = Mapper3D.CAMERA_ROTATION_FACTOR_INIT; 52 private zSpacingFactor = Mapper3D.Z_SPACING_FACTOR_INIT; 53 private zoomFactor = Mapper3D.ZOOM_FACTOR_INIT; 54 private panScreenDistance: Distance2D = new Distance2D(0, 0); 55 private showOnlyVisibleMode = false; // by default show all 56 private showVirtualMode = false; // by default don't show virtual displays 57 private currentDisplayId = 0; // default stack id is usually 0 58 59 setRects(rects: Rectangle[]) { 60 this.rects = rects; 61 } 62 63 setHighlightedRectIds(ids: string[]) { 64 this.highlightedRectIds = ids; 65 } 66 67 getCameraRotationFactor(): number { 68 return this.cameraRotationFactor; 69 } 70 71 setCameraRotationFactor(factor: number) { 72 this.cameraRotationFactor = Math.min(Math.max(factor, 0), 1); 73 } 74 75 getZSpacingFactor(): number { 76 return this.zSpacingFactor; 77 } 78 79 setZSpacingFactor(factor: number) { 80 this.zSpacingFactor = Math.min(Math.max(factor, 0), 1); 81 } 82 83 increaseZoomFactor() { 84 this.zoomFactor += Mapper3D.ZOOM_FACTOR_STEP; 85 this.zoomFactor = Math.min(this.zoomFactor, Mapper3D.ZOOM_FACTOR_MAX); 86 } 87 88 decreaseZoomFactor() { 89 this.zoomFactor -= Mapper3D.ZOOM_FACTOR_STEP; 90 this.zoomFactor = Math.max(this.zoomFactor, Mapper3D.ZOOM_FACTOR_MIN); 91 } 92 93 addPanScreenDistance(distance: Distance2D) { 94 this.panScreenDistance.dx += distance.dx; 95 this.panScreenDistance.dy += distance.dy; 96 } 97 98 resetCamera() { 99 this.cameraRotationFactor = Mapper3D.CAMERA_ROTATION_FACTOR_INIT; 100 this.zSpacingFactor = Mapper3D.Z_SPACING_FACTOR_INIT; 101 this.zoomFactor = Mapper3D.ZOOM_FACTOR_INIT; 102 this.panScreenDistance.dx = 0; 103 this.panScreenDistance.dy = 0; 104 } 105 106 getShowOnlyVisibleMode(): boolean { 107 return this.showOnlyVisibleMode; 108 } 109 110 setShowOnlyVisibleMode(enabled: boolean) { 111 this.showOnlyVisibleMode = enabled; 112 } 113 114 getShowVirtualMode(): boolean { 115 return this.showVirtualMode; 116 } 117 118 setShowVirtualMode(enabled: boolean) { 119 this.showVirtualMode = enabled; 120 } 121 122 getCurrentDisplayId(): number { 123 return this.currentDisplayId; 124 } 125 126 setCurrentDisplayId(id: number) { 127 this.currentDisplayId = id; 128 } 129 130 computeScene(): Scene3D { 131 const rects2d = this.selectRectsToDraw(this.rects); 132 const rects3d = this.computeRects(rects2d); 133 const labels3d = this.computeLabels(rects2d, rects3d); 134 const boundingBox = this.computeBoundingBox(rects3d, labels3d); 135 136 const scene: Scene3D = { 137 boundingBox, 138 camera: { 139 rotationFactor: this.cameraRotationFactor, 140 zoomFactor: this.zoomFactor, 141 panScreenDistance: this.panScreenDistance, 142 }, 143 rects: rects3d, 144 labels: labels3d, 145 }; 146 147 return scene; 148 } 149 150 private selectRectsToDraw(rects: Rectangle[]): Rectangle[] { 151 rects = rects.filter((rect) => rect.displayId === this.currentDisplayId); 152 153 if (this.showOnlyVisibleMode) { 154 rects = rects.filter((rect) => rect.isVisible || rect.isDisplay); 155 } 156 157 if (!this.showVirtualMode) { 158 rects = rects.filter((rect) => !rect.isVirtual); 159 } 160 161 return rects; 162 } 163 164 private computeRects(rects2d: Rectangle[]): Rect3D[] { 165 let visibleRectsSoFar = 0; 166 let visibleRectsTotal = 0; 167 let nonVisibleRectsSoFar = 0; 168 let nonVisibleRectsTotal = 0; 169 170 rects2d.forEach((rect) => { 171 if (rect.isVisible) { 172 ++visibleRectsTotal; 173 } else { 174 ++nonVisibleRectsTotal; 175 } 176 }); 177 178 let z = 0; 179 180 const maxDisplaySize = this.getMaxDisplaySize(rects2d); 181 182 const rects3d = rects2d.map((rect2d): Rect3D => { 183 if (rect2d.depth !== undefined) { 184 z = Mapper3D.Z_SPACING_MAX * this.zSpacingFactor * rect2d.depth; 185 } else { 186 z -= Mapper3D.Z_SPACING_MAX * this.zSpacingFactor; 187 } 188 189 const darkFactor = rect2d.isVisible 190 ? (visibleRectsTotal - visibleRectsSoFar++) / visibleRectsTotal 191 : (nonVisibleRectsTotal - nonVisibleRectsSoFar++) / nonVisibleRectsTotal; 192 193 const rect = { 194 id: rect2d.id, 195 topLeft: { 196 x: rect2d.topLeft.x, 197 y: rect2d.topLeft.y, 198 z, 199 }, 200 bottomRight: { 201 x: rect2d.bottomRight.x, 202 y: rect2d.bottomRight.y, 203 z, 204 }, 205 isOversized: false, 206 cornerRadius: rect2d.cornerRadius, 207 darkFactor, 208 colorType: this.getColorType(rect2d), 209 isClickable: rect2d.isClickable, 210 transform: this.getTransform(rect2d), 211 }; 212 return this.cropOversizedRect(rect, maxDisplaySize); 213 }); 214 215 return rects3d; 216 } 217 218 private getColorType(rect2d: Rectangle): ColorType { 219 let colorType: ColorType; 220 if (this.highlightedRectIds.includes(rect2d.id)) { 221 colorType = ColorType.HIGHLIGHTED; 222 } else if (rect2d.isVisible) { 223 colorType = ColorType.VISIBLE; 224 } else { 225 colorType = ColorType.NOT_VISIBLE; 226 } 227 return colorType; 228 } 229 230 private getTransform(rect2d: Rectangle): Transform3D { 231 let transform: Transform3D; 232 if (rect2d.transform?.matrix) { 233 transform = { 234 dsdx: rect2d.transform.matrix.dsdx, 235 dsdy: rect2d.transform.matrix.dsdy, 236 tx: rect2d.transform.matrix.tx, 237 dtdx: rect2d.transform.matrix.dtdx, 238 dtdy: rect2d.transform.matrix.dtdy, 239 ty: rect2d.transform.matrix.ty, 240 }; 241 } else { 242 transform = Mapper3D.IDENTITY_TRANSFORM; 243 } 244 return transform; 245 } 246 247 private getMaxDisplaySize(rects2d: Rectangle[]): Size { 248 const displays = rects2d.filter((rect2d) => rect2d.isDisplay); 249 250 let maxWidth = 0; 251 let maxHeight = 0; 252 if (displays.length > 0) { 253 maxWidth = Math.max( 254 ...displays.map((rect2d): number => Math.abs(rect2d.topLeft.x - rect2d.bottomRight.x)) 255 ); 256 257 maxHeight = Math.max( 258 ...displays.map((rect2d): number => Math.abs(rect2d.topLeft.y - rect2d.bottomRight.y)) 259 ); 260 } 261 return { 262 width: maxWidth, 263 height: maxHeight, 264 }; 265 } 266 267 private cropOversizedRect(rect3d: Rect3D, maxDisplaySize: Size): Rect3D { 268 // Arbitrary max size for a rect (2x the maximum display) 269 let maxDimension = Number.MAX_VALUE; 270 if (maxDisplaySize.height > 0) { 271 maxDimension = Math.max(maxDisplaySize.width, maxDisplaySize.height) * 2; 272 } 273 274 const height = Math.abs(rect3d.topLeft.y - rect3d.bottomRight.y); 275 const width = Math.abs(rect3d.topLeft.x - rect3d.bottomRight.x); 276 277 if (width > maxDimension) { 278 rect3d.isOversized = true; 279 (rect3d.topLeft.x = (maxDimension - maxDisplaySize.width / 2) * -1), 280 (rect3d.bottomRight.x = maxDimension); 281 } 282 if (height > maxDimension) { 283 rect3d.isOversized = true; 284 rect3d.topLeft.y = (maxDimension - maxDisplaySize.height / 2) * -1; 285 rect3d.bottomRight.y = maxDimension; 286 } 287 288 return rect3d; 289 } 290 291 private computeLabels(rects2d: Rectangle[], rects3d: Rect3D[]): Label3D[] { 292 const labels3d: Label3D[] = []; 293 294 let labelY = 295 Math.max( 296 ...rects3d.map((rect) => { 297 return this.matMultiply(rect.transform, rect.bottomRight).y; 298 }) 299 ) + Mapper3D.LABEL_FIRST_Y_OFFSET; 300 301 rects2d.forEach((rect2d, index) => { 302 if (!rect2d.label) { 303 return; 304 } 305 306 const rect3d = rects3d[index]; 307 308 const bottomLeft: Point3D = { 309 x: rect3d.topLeft.x, 310 y: rect3d.bottomRight.y, 311 z: rect3d.topLeft.z, 312 }; 313 const topRight: Point3D = { 314 x: rect3d.bottomRight.x, 315 y: rect3d.topLeft.y, 316 z: rect3d.bottomRight.z, 317 }; 318 const lineStarts = [ 319 this.matMultiply(rect3d.transform, rect3d.topLeft), 320 this.matMultiply(rect3d.transform, rect3d.bottomRight), 321 this.matMultiply(rect3d.transform, bottomLeft), 322 this.matMultiply(rect3d.transform, topRight), 323 ]; 324 let maxIndex = 0; 325 for (let i = 1; i < lineStarts.length; i++) { 326 if (lineStarts[i].x > lineStarts[maxIndex].x) { 327 maxIndex = i; 328 } 329 } 330 const lineStart = lineStarts[maxIndex]; 331 lineStart.x += Mapper3D.LABEL_CIRCLE_RADIUS / 2; 332 333 const lineEnd: Point3D = { 334 x: lineStart.x, 335 y: labelY, 336 z: lineStart.z, 337 }; 338 339 const isHighlighted = this.highlightedRectIds.includes(rect2d.id); 340 341 const label3d: Label3D = { 342 circle: { 343 radius: Mapper3D.LABEL_CIRCLE_RADIUS, 344 center: { 345 x: lineStart.x, 346 y: lineStart.y, 347 z: lineStart.z + 0.5, 348 }, 349 }, 350 linePoints: [lineStart, lineEnd], 351 textCenter: lineEnd, 352 text: rect2d.label, 353 isHighlighted, 354 rectId: rect2d.id, 355 }; 356 labels3d.push(label3d); 357 358 labelY += Mapper3D.LABEL_TEXT_Y_SPACING; 359 }); 360 361 return labels3d; 362 } 363 364 private matMultiply(mat: Transform3D, point: Point3D): Point3D { 365 return { 366 x: mat.dsdx * point.x + mat.dsdy * point.y + mat.tx, 367 y: mat.dtdx * point.x + mat.dtdy * point.y + mat.ty, 368 z: point.z, 369 }; 370 } 371 372 private computeBoundingBox(rects: Rect3D[], labels: Label3D[]): Box3D { 373 if (rects.length === 0) { 374 return { 375 width: 1, 376 height: 1, 377 depth: 1, 378 center: {x: 0, y: 0, z: 0}, 379 diagonal: Math.sqrt(3), 380 }; 381 } 382 383 let minX = Number.MAX_VALUE; 384 let maxX = Number.MIN_VALUE; 385 let minY = Number.MAX_VALUE; 386 let maxY = Number.MIN_VALUE; 387 let minZ = Number.MAX_VALUE; 388 let maxZ = Number.MIN_VALUE; 389 390 const updateMinMaxCoordinates = (point: Point3D) => { 391 minX = Math.min(minX, point.x); 392 maxX = Math.max(maxX, point.x); 393 minY = Math.min(minY, point.y); 394 maxY = Math.max(maxY, point.y); 395 minZ = Math.min(minZ, point.z); 396 maxZ = Math.max(maxZ, point.z); 397 }; 398 399 rects.forEach((rect) => { 400 /*const topLeft: Point3D = { 401 x: rect.center.x - rect.width / 2, 402 y: rect.center.y + rect.height / 2, 403 z: rect.center.z 404 }; 405 const bottomRight: Point3D = { 406 x: rect.center.x + rect.width / 2, 407 y: rect.center.y - rect.height / 2, 408 z: rect.center.z 409 };*/ 410 updateMinMaxCoordinates(rect.topLeft); 411 updateMinMaxCoordinates(rect.bottomRight); 412 }); 413 414 labels.forEach((label) => { 415 label.linePoints.forEach((point) => { 416 updateMinMaxCoordinates(point); 417 }); 418 }); 419 420 const center: Point3D = { 421 x: (minX + maxX) / 2, 422 y: (minY + maxY) / 2, 423 z: (minZ + maxZ) / 2, 424 }; 425 426 const width = maxX - minX; 427 const height = maxY - minY; 428 const depth = maxZ - minZ; 429 430 return { 431 width, 432 height, 433 depth, 434 center, 435 diagonal: Math.sqrt(width * width + height * height + depth * depth), 436 }; 437 } 438} 439 440export {Mapper3D}; 441