• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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