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