UNPKG

@js-draw/math

Version:
331 lines (330 loc) 11.8 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Rect2 = void 0; const LineSegment2_1 = __importDefault(require("./LineSegment2")); const Mat33_1 = __importDefault(require("../Mat33")); const Vec2_1 = require("../Vec2"); const Abstract2DShape_1 = __importDefault(require("./Abstract2DShape")); /** * Represents a rectangle in 2D space, parallel to the XY axes. * * **Example**: * ```ts,runnable,console * import { Rect2, Vec2 } from '@js-draw/math'; * * const rect = Rect2.fromCorners( * Vec2.of(0, 0), * Vec2.of(10, 10), * ); * console.log('area', rect.area); * console.log('topLeft', rect.topLeft); * ``` * * `invariant: w ≥ 0, h ≥ 0, immutable` */ class Rect2 extends Abstract2DShape_1.default { constructor( // Top left x coordinate x, // Top left y coordinate y, // Width w, // Height h) { super(); this.x = x; this.y = y; this.w = w; this.h = h; if (w < 0) { this.x += w; this.w = Math.abs(w); } if (h < 0) { this.y += h; this.h = Math.abs(h); } // Precompute/store vector forms. this.topLeft = Vec2_1.Vec2.of(this.x, this.y); this.size = Vec2_1.Vec2.of(this.w, this.h); this.area = this.w * this.h; } translatedBy(vec) { return new Rect2(vec.x + this.x, vec.y + this.y, this.w, this.h); } // Returns a copy of this with the given size (but same top-left). resizedTo(size) { return new Rect2(this.x, this.y, size.x, size.y); } containsPoint(other) { return (this.x <= other.x && this.y <= other.y && this.x + this.w >= other.x && this.y + this.h >= other.y); } /** @returns true iff `other` is completely within this `Rect2`. */ containsRect(other) { return (this.x <= other.x && this.y <= other.y && this.x + this.w >= other.x + other.w && this.y + this.h >= other.y + other.h); } /** * @returns true iff this and `other` overlap */ intersects(other) { // Project along x/y axes. const thisMinX = this.x; const thisMaxX = thisMinX + this.w; const otherMinX = other.x; const otherMaxX = other.x + other.w; if (thisMaxX < otherMinX || thisMinX > otherMaxX) { return false; } const thisMinY = this.y; const thisMaxY = thisMinY + this.h; const otherMinY = other.y; const otherMaxY = other.y + other.h; if (thisMaxY < otherMinY || thisMinY > otherMaxY) { return false; } return true; } // Returns the overlap of this and [other], or null, if no such // overlap exists intersection(other) { if (!this.intersects(other)) { return null; } const topLeft = this.topLeft.zip(other.topLeft, Math.max); const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min); return Rect2.fromCorners(topLeft, bottomRight); } // Returns a new rectangle containing both [this] and [other]. union(other) { return Rect2.union(this, other); } // Returns a the subdivision of this into [columns] columns // and [rows] rows. For example, // Rect2.unitSquare.divideIntoGrid(2, 2) // -> [ Rect2(0, 0, 0.5, 0.5), Rect2(0.5, 0, 0.5, 0.5), Rect2(0, 0.5, 0.5, 0.5), Rect2(0.5, 0.5, 0.5, 0.5) ] // The rectangles are ordered in row-major order. divideIntoGrid(columns, rows) { const result = []; if (columns <= 0 || rows <= 0) { return result; } const eachRectWidth = this.w / columns; const eachRectHeight = this.h / rows; if (eachRectWidth === 0) { columns = 1; } if (eachRectHeight === 0) { rows = 1; } for (let j = 0; j < rows; j++) { for (let i = 0; i < columns; i++) { const x = eachRectWidth * i + this.x; const y = eachRectHeight * j + this.y; result.push(new Rect2(x, y, eachRectWidth, eachRectHeight)); } } return result; } // Returns a rectangle containing this and [point]. // [margin] is the minimum distance between the new point and the edge // of the resultant rectangle. grownToPoint(point, margin = 0) { const otherRect = new Rect2(point.x - margin, point.y - margin, margin * 2, margin * 2); return this.union(otherRect); } // Returns this grown by [margin] in both the x and y directions. grownBy(margin) { if (margin === 0) { return this; } // Prevent width/height from being negative if (margin < 0) { const xMargin = -Math.min(-margin, this.w / 2); const yMargin = -Math.min(-margin, this.h / 2); return new Rect2(this.x - xMargin, this.y - yMargin, this.w + xMargin * 2, this.h + yMargin * 2); } return new Rect2(this.x - margin, this.y - margin, this.w + margin * 2, this.h + margin * 2); } /** * If this rectangle is smaller than `minSize`, returns a copy of this * with a larger width/height. * * If smaller than `minSize`, padding is applied on both sides. */ grownToSize(minSize) { if (this.width >= minSize.x && this.height >= minSize.y) { return this; } const deltaWidth = Math.max(0, minSize.x - this.width); const deltaHeight = Math.max(0, minSize.y - this.height); return new Rect2(this.x - deltaWidth / 2, this.y - deltaHeight / 2, this.width + deltaWidth, this.height + deltaHeight); } getClosestPointOnBoundaryTo(target) { const closestEdgePoints = this.getEdges().map((edge) => { return edge.closestPointTo(target); }); let closest = null; let closestDist = null; for (const point of closestEdgePoints) { const dist = point.distanceTo(target); if (closestDist === null || dist < closestDist) { closest = point; closestDist = dist; } } return closest; } /** * Returns `true` iff all points in this rectangle are within `distance` from `point`: * * If $R$ is the set of points in this rectangle, returns `true` iff * $$ * \forall {\bf a} \in R, \|\texttt{point} - {\bf a}\| < \texttt{radius} * $$ */ isWithinRadiusOf(radius, point) { if (this.maxDimension > radius) { return false; } const squareRadius = radius * radius; return this.corners.every((corner) => corner.minus(point).magnitudeSquared() < squareRadius); } get corners() { return [this.bottomRight, this.topRight, this.topLeft, this.bottomLeft]; } get maxDimension() { return Math.max(this.w, this.h); } get minDimension() { return Math.min(this.w, this.h); } get bottomRight() { return this.topLeft.plus(this.size); } get topRight() { return this.bottomRight.plus(Vec2_1.Vec2.of(0, -this.h)); } get bottomLeft() { return this.topLeft.plus(Vec2_1.Vec2.of(0, this.h)); } get width() { return this.w; } get height() { return this.h; } get center() { return Vec2_1.Vec2.of(this.x + this.w / 2, this.y + this.h / 2); } // Returns edges in the order // [ rightEdge, topEdge, leftEdge, bottomEdge ] getEdges() { const corners = this.corners; return [ new LineSegment2_1.default(corners[0], corners[1]), new LineSegment2_1.default(corners[1], corners[2]), new LineSegment2_1.default(corners[2], corners[3]), new LineSegment2_1.default(corners[3], corners[0]), ]; } intersectsLineSegment(lineSegment) { const result = []; for (const edge of this.getEdges()) { const intersection = edge.intersectsLineSegment(lineSegment); intersection.forEach((point) => result.push(point)); } return result; } signedDistance(point) { const closestBoundaryPoint = this.getClosestPointOnBoundaryTo(point); const dist = point.minus(closestBoundaryPoint).magnitude(); if (this.containsPoint(point)) { return -dist; } return dist; } getTightBoundingBox() { return this; } // [affineTransform] is a transformation matrix that both scales and **translates**. // the bounding box of this' four corners after transformed by the given affine transformation. transformedBoundingBox(affineTransform) { // Optimize transforming by the identity matrix (a common case). if (affineTransform === Mat33_1.default.identity) { return this; } return Rect2.bboxOf(this.corners.map((corner) => affineTransform.transformVec2(corner))); } /** @return true iff this is equal to `other ± tolerance` */ eq(other, tolerance = 0) { return this.topLeft.eq(other.topLeft, tolerance) && this.size.eq(other.size, tolerance); } toString() { return `Rect(point(${this.x}, ${this.y}), size(${this.w}, ${this.h}))`; } static fromCorners(corner1, corner2) { return new Rect2(Math.min(corner1.x, corner2.x), Math.min(corner1.y, corner2.y), Math.abs(corner1.x - corner2.x), Math.abs(corner1.y - corner2.y)); } // Returns a box that contains all points in [points] with at least [margin] // between each point and the edge of the box. static bboxOf(points, margin = 0) { let minX = 0; let minY = 0; let maxX = 0; let maxY = 0; let isFirst = true; for (const point of points) { if (isFirst) { minX = point.x; minY = point.y; maxX = point.x; maxY = point.y; isFirst = false; } minX = Math.min(minX, point.x); minY = Math.min(minY, point.y); maxX = Math.max(maxX, point.x); maxY = Math.max(maxY, point.y); } return Rect2.fromCorners(Vec2_1.Vec2.of(minX - margin, minY - margin), Vec2_1.Vec2.of(maxX + margin, maxY + margin)); } // @returns a rectangle that contains all of the given rectangles, the bounding box // of the given rectangles. static union(...rects) { if (rects.length === 0) { return Rect2.empty; } const firstRect = rects[0]; let minX = firstRect.x; let minY = firstRect.y; let maxX = firstRect.x + firstRect.w; let maxY = firstRect.y + firstRect.h; for (let i = 1; i < rects.length; i++) { const rect = rects[i]; minX = Math.min(minX, rect.x); minY = Math.min(minY, rect.y); maxX = Math.max(maxX, rect.x + rect.w); maxY = Math.max(maxY, rect.y + rect.h); } return new Rect2(minX, minY, maxX - minX, maxY - minY); } static of(template) { const width = template.width ?? template.w ?? 0; const height = template.height ?? template.h ?? 0; return new Rect2(template.x, template.y, width, height); } } exports.Rect2 = Rect2; Rect2.empty = new Rect2(0, 0, 0, 0); Rect2.unitSquare = new Rect2(0, 0, 1, 1); exports.default = Rect2;