• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright (C) 2024 The Android Open Source Project
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//      http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15// This library provides interfaces and classes for handling 2D geometry
16// operations.
17
18/**
19 * Interface representing a point in 2D space.
20 */
21export interface Point2D {
22  readonly x: number;
23  readonly y: number;
24}
25
26/**
27 * Class representing a 2D vector with methods for vector operations.
28 *
29 * Note: This class is immutable in TypeScript (not enforced at runtime). Any
30 * method that modifies the vector returns a new instance, leaving the original
31 * unchanged.
32 */
33export class Vector2D implements Point2D {
34  readonly x: number;
35  readonly y: number;
36
37  constructor({x, y}: Point2D) {
38    this.x = x;
39    this.y = y;
40  }
41
42  /**
43   * Adds the given point to this vector and returns a new vector.
44   *
45   * @param point - The point to add.
46   * @returns A new Vector2D instance representing the result.
47   */
48  add(point: Point2D): Vector2D {
49    return new Vector2D({x: this.x + point.x, y: this.y + point.y});
50  }
51
52  /**
53   * Subtracts the given point from this vector and returns a new vector.
54   *
55   * @param point - The point to subtract.
56   * @returns A new Vector2D instance representing the result.
57   */
58  sub(point: Point2D): Vector2D {
59    return new Vector2D({x: this.x - point.x, y: this.y - point.y});
60  }
61
62  /**
63   * Scales this vector by the given scalar and returns a new vector.
64   *
65   * @param scalar - The scalar value to multiply the vector by.
66   * @returns A new Vector2D instance representing the scaled vector.
67   */
68  scale(scalar: number): Vector2D {
69    return new Vector2D({x: this.x * scalar, y: this.y * scalar});
70  }
71
72  /**
73   * Computes the Manhattan distance, which is the sum of the absolute values of
74   * the x and y components of the vector. This represents the distance
75   * travelled along axes at right angles (grid-based distance).
76   */
77  get manhattanDistance(): number {
78    return Math.abs(this.x) + Math.abs(this.y);
79  }
80
81  /**
82   * Computes the Euclidean magnitude (or length) of the vector. This is the
83   * straight-line distance from the origin (0, 0) to the point (x, y) in 2D
84   * space.
85   */
86  get magnitude(): number {
87    return Math.sqrt(this.x * this.x + this.y * this.y);
88  }
89}
90
91/**
92 * Interface representing the vertical bounds of an object (top and bottom).
93 */
94export interface VerticalBounds {
95  readonly top: number;
96  readonly bottom: number;
97}
98
99/**
100 * Interface representing the horizontal bounds of an object (left and right).
101 */
102export interface HorizontalBounds {
103  readonly left: number;
104  readonly right: number;
105}
106
107/**
108 * Interface combining vertical and horizontal bounds to describe a 2D bounding
109 * box.
110 */
111export interface Bounds2D extends VerticalBounds, HorizontalBounds {}
112
113/**
114 * Interface representing the size of a 2D object.
115 */
116export interface Size2D {
117  readonly width: number;
118  readonly height: number;
119}
120
121/**
122 * Immutable class representing a 2D rectangle with a 2D position and size which
123 * has functions to mutate and test the rect and can be polymorphically used as
124 * any of the following:
125 * - Bounds2D
126 * - Size2D
127 * - Point2D
128 */
129export class Rect2D implements Bounds2D, Size2D, Point2D {
130  readonly left: number;
131  readonly top: number;
132  readonly right: number;
133  readonly bottom: number;
134  readonly x: number; // Always equal to left
135  readonly y: number; // Always equal to top
136  readonly width: number; // Always equal to (right - left)
137  readonly height: number; // Always equal to (bottom - top)
138
139  /**
140   * Creates a new rect from two points, automatically ordering them to avoid
141   * negative rect dimensions.
142   *
143   * E.g. Rect2D.fromPoints({x: 10, y: 20}, {x: 20, y: 25})
144   *
145   * @returns A new Rect2D object.
146   */
147  static fromPoints(a: Point2D, b: Point2D) {
148    return new Rect2D({
149      top: Math.min(a.y, b.y),
150      left: Math.min(a.x, b.x),
151      right: Math.max(a.x, b.x),
152      bottom: Math.max(a.y, b.y),
153    });
154  }
155
156  /**
157   * Creates a new rect given a point and size.
158   *
159   * E.g. Rect2D.fromPointAndSize({x: 10, y: 20, width: 100, height: 80})
160   *
161   * @param pointAndSize - The combined point and size.
162   * @returns A new Rect2D object.
163   */
164  static fromPointAndSize(pointAndSize: Point2D & Size2D) {
165    const {x, y, width, height} = pointAndSize;
166    return new Rect2D({
167      top: y,
168      left: x,
169      right: x + width,
170      bottom: y + height,
171    });
172  }
173
174  constructor({left, top, right, bottom}: Bounds2D) {
175    this.left = this.x = left;
176    this.top = this.y = top;
177    this.right = right;
178    this.bottom = bottom;
179    this.width = right - left;
180    this.height = bottom - top;
181  }
182
183  /**
184   * Returns a new rectangle representing the intersection with another
185   * rectangle.
186   *
187   * @param bounds - The bounds of the other rectangle to intersect with.
188   * @returns A new Rect2D instance representing the intersected rectangle.
189   */
190  intersect(bounds: Bounds2D): Rect2D {
191    return new Rect2D({
192      top: Math.max(this.top, bounds.top),
193      left: Math.max(this.left, bounds.left),
194      bottom: Math.min(this.bottom, bounds.bottom),
195      right: Math.min(this.right, bounds.right),
196    });
197  }
198
199  /**
200   * Expands the rectangle by the given amount on all sides and returns a new
201   * rectangle.
202   *
203   * @param amount - The amount to expand the rectangle by. This can be a number
204   * which is applied evenly to each side, or it can be a Size2D object which
205   * applies a different expansion amount in the x and y dimensions.
206   * @returns A new Rect2D instance representing the expanded rectangle.
207   */
208  expand(amount: number | Size2D): Rect2D {
209    if (typeof amount === 'number') {
210      return new Rect2D({
211        top: this.top - amount,
212        left: this.left - amount,
213        bottom: this.bottom + amount,
214        right: this.right + amount,
215      });
216    } else {
217      const {width, height} = amount;
218      return new Rect2D({
219        top: this.top - height,
220        left: this.left - width,
221        bottom: this.bottom + height,
222        right: this.right + width,
223      });
224    }
225  }
226
227  /**
228   * Reframes the rectangle by shifting its origin by the given point.
229   *
230   * @param point - The point by which to shift the origin.
231   * @returns A new Rect2D instance representing the reframed rectangle.
232   */
233  reframe(point: Point2D): Rect2D {
234    return new Rect2D({
235      left: this.left - point.x,
236      right: this.right - point.x,
237      top: this.top - point.y,
238      bottom: this.bottom - point.y,
239    });
240  }
241
242  /**
243   * Checks if this rectangle fully contains another set of bounds.
244   *
245   * @param bounds - The bounds to check containment for.
246   * @returns True if this rectangle contains the given bounds, false otherwise.
247   */
248  contains(bounds: Bounds2D): boolean {
249    return !(
250      bounds.top < this.top ||
251      bounds.bottom > this.bottom ||
252      bounds.left < this.left ||
253      bounds.right > this.right
254    );
255  }
256
257  /**
258   * Checks if this rectangle contains a point in 2D space.
259   *
260   * @param point - The point to check.
261   * @returns True if this rectangle contains the given point, false otherwise.
262   */
263  containsPoint(point: Point2D): boolean {
264    return (
265      point.y >= this.top &&
266      point.y < this.bottom &&
267      point.x >= this.left &&
268      point.x < this.right
269    );
270  }
271
272  /**
273   * Checks if this rectangle overlaps another set of bounds.
274   *
275   * @param bounds - The bounds to check overlap for.
276   * @returns rue if this rectangle overlaps the given bounds, false otherwise.
277   */
278  overlaps(bounds: Bounds2D): boolean {
279    return (
280      this.left < bounds.right &&
281      this.right > bounds.left &&
282      this.top < bounds.bottom &&
283      this.bottom > bounds.top
284    );
285  }
286
287  /**
288   * Translates the rectangle by the given point and returns a new rectangle.
289   *
290   * @param point - The point by which to translate the rectangle.
291   * @returns A new Rect2D instance representing the translated rectangle.
292   */
293  translate(point: Point2D): Rect2D {
294    return new Rect2D({
295      top: this.top + point.y,
296      left: this.left + point.x,
297      bottom: this.bottom + point.y,
298      right: this.right + point.x,
299    });
300  }
301
302  equals(bounds: Bounds2D): boolean {
303    return (
304      bounds.top === this.top &&
305      bounds.left === this.left &&
306      bounds.right === this.right &&
307      bounds.bottom === this.bottom
308    );
309  }
310}
311