// Copyright (C) 2024 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This library provides interfaces and classes for handling 2D geometry // operations. /** * Interface representing a point in 2D space. */ export interface Point2D { readonly x: number; readonly y: number; } /** * Class representing a 2D vector with methods for vector operations. * * Note: This class is immutable in TypeScript (not enforced at runtime). Any * method that modifies the vector returns a new instance, leaving the original * unchanged. */ export class Vector2D implements Point2D { readonly x: number; readonly y: number; constructor({x, y}: Point2D) { this.x = x; this.y = y; } /** * Adds the given point to this vector and returns a new vector. * * @param point - The point to add. * @returns A new Vector2D instance representing the result. */ add(point: Point2D): Vector2D { return new Vector2D({x: this.x + point.x, y: this.y + point.y}); } /** * Subtracts the given point from this vector and returns a new vector. * * @param point - The point to subtract. * @returns A new Vector2D instance representing the result. */ sub(point: Point2D): Vector2D { return new Vector2D({x: this.x - point.x, y: this.y - point.y}); } /** * Scales this vector by the given scalar and returns a new vector. * * @param scalar - The scalar value to multiply the vector by. * @returns A new Vector2D instance representing the scaled vector. */ scale(scalar: number): Vector2D { return new Vector2D({x: this.x * scalar, y: this.y * scalar}); } /** * Computes the Manhattan distance, which is the sum of the absolute values of * the x and y components of the vector. This represents the distance * travelled along axes at right angles (grid-based distance). */ get manhattanDistance(): number { return Math.abs(this.x) + Math.abs(this.y); } /** * Computes the Euclidean magnitude (or length) of the vector. This is the * straight-line distance from the origin (0, 0) to the point (x, y) in 2D * space. */ get magnitude(): number { return Math.sqrt(this.x * this.x + this.y * this.y); } } /** * Interface representing the vertical bounds of an object (top and bottom). */ export interface VerticalBounds { readonly top: number; readonly bottom: number; } /** * Interface representing the horizontal bounds of an object (left and right). */ export interface HorizontalBounds { readonly left: number; readonly right: number; } /** * Interface combining vertical and horizontal bounds to describe a 2D bounding * box. */ export interface Bounds2D extends VerticalBounds, HorizontalBounds {} /** * Interface representing the size of a 2D object. */ export interface Size2D { readonly width: number; readonly height: number; } /** * Class representing a 2D rectangle, implementing bounds and size interfaces. */ export class Rect2D implements Bounds2D, Size2D { readonly left: number; readonly top: number; readonly right: number; readonly bottom: number; readonly width: number; readonly height: number; constructor({left, top, right, bottom}: Bounds2D) { this.left = left; this.top = top; this.right = right; this.bottom = bottom; this.width = right - left; this.height = bottom - top; } /** * Returns a new rectangle representing the intersection with another * rectangle. * * @param bounds - The bounds of the other rectangle to intersect with. * @returns A new Rect2D instance representing the intersected rectangle. */ intersect(bounds: Bounds2D): Rect2D { return new Rect2D({ top: Math.max(this.top, bounds.top), left: Math.max(this.left, bounds.left), bottom: Math.min(this.bottom, bounds.bottom), right: Math.min(this.right, bounds.right), }); } /** * Expands the rectangle by the given amount on all sides and returns a new * rectangle. * * @param amount - The amount to expand the rectangle by. * @returns A new Rect2D instance representing the expanded rectangle. */ expand(amount: number): Rect2D { return new Rect2D({ top: this.top - amount, left: this.left - amount, bottom: this.bottom + amount, right: this.right + amount, }); } /** * Reframes the rectangle by shifting its origin by the given point. * * @param point - The point by which to shift the origin. * @returns A new Rect2D instance representing the reframed rectangle. */ reframe(point: Point2D): Rect2D { return new Rect2D({ left: this.left - point.x, right: this.right - point.x, top: this.top - point.y, bottom: this.bottom - point.y, }); } /** * Checks if this rectangle fully contains another set of bounds. * * @param bounds - The bounds to check containment for. * @returns True if this rectangle contains the given bounds, false otherwise. */ contains(bounds: Bounds2D): boolean { return !( bounds.top < this.top || bounds.bottom > this.bottom || bounds.left < this.left || bounds.right > this.right ); } /** * Translates the rectangle by the given point and returns a new rectangle. * * @param point - The point by which to translate the rectangle. * @returns A new Rect2D instance representing the translated rectangle. */ translate(point: Point2D): Rect2D { return new Rect2D({ top: this.top + point.y, left: this.left + point.x, bottom: this.bottom + point.y, right: this.right + point.x, }); } }