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