UNPKG

@tldraw/editor

Version:

tldraw infinite canvas SDK (editor).

732 lines (623 loc) • 21.8 kB
import { Box } from './Box' import { Vec } from './Vec' describe('Box', () => { let box: Box beforeEach(() => { box = new Box(10, 20, 100, 200) }) it('Creates a box', () => { const newBox = new Box(0, 0, 100, 100) expect(newBox).toMatchObject({ x: 0, y: 0, w: 100, h: 100, }) }) describe('Box.point', () => { it('gets the point as a Vec', () => { expect(box.point).toEqual(new Vec(10, 20)) }) it('sets the point with a Vec', () => { box.point = new Vec(50, 60) expect(box.x).toBe(50) expect(box.y).toBe(60) }) }) describe('Box.minX', () => { it('gets the minimum X value', () => { expect(box.minX).toBe(10) }) it('sets the minimum X value', () => { box.minX = 30 expect(box.x).toBe(30) }) }) describe('Box.left', () => { it('gets the left edge', () => { expect(box.left).toBe(10) }) }) describe('Box.midX', () => { it('gets the middle X value', () => { expect(box.midX).toBe(60) // 10 + 100/2 }) }) describe('Box.maxX', () => { it('gets the maximum X value', () => { expect(box.maxX).toBe(110) // 10 + 100 }) }) describe('Box.right', () => { it('gets the right edge', () => { expect(box.right).toBe(110) // 10 + 100 }) }) describe('Box.minY', () => { it('gets the minimum Y value', () => { expect(box.minY).toBe(20) }) it('sets the minimum Y value', () => { box.minY = 40 expect(box.y).toBe(40) }) }) describe('Box.top', () => { it('gets the top edge', () => { expect(box.top).toBe(20) }) }) describe('Box.midY', () => { it('gets the middle Y value', () => { expect(box.midY).toBe(120) // 20 + 200/2 }) }) describe('Box.maxY', () => { it('gets the maximum Y value', () => { expect(box.maxY).toBe(220) // 20 + 200 }) }) describe('Box.bottom', () => { it('gets the bottom edge', () => { expect(box.bottom).toBe(220) // 20 + 200 }) }) describe('Box.width', () => { it('gets the width', () => { expect(box.width).toBe(100) }) it('sets the width', () => { box.width = 150 expect(box.w).toBe(150) }) }) describe('Box.height', () => { it('gets the height', () => { expect(box.height).toBe(200) }) it('sets the height', () => { box.height = 250 expect(box.h).toBe(250) }) }) describe('Box.aspectRatio', () => { it('gets the aspect ratio', () => { expect(box.aspectRatio).toBe(0.5) // 100/200 }) }) describe('Box.center', () => { it('gets the center point', () => { expect(box.center).toEqual(new Vec(60, 120)) }) it('sets the center point', () => { box.center = new Vec(100, 150) expect(box.x).toBe(50) // 100 - 100/2 expect(box.y).toBe(50) // 150 - 200/2 }) }) describe('Box.corners', () => { it('gets the corners', () => { const corners = box.corners expect(corners).toEqual([ new Vec(10, 20), // top-left new Vec(110, 20), // top-right (fixed from buggy bottom-right) new Vec(110, 220), // bottom-right new Vec(10, 220), // bottom-left ]) }) }) describe('Box.cornersAndCenter', () => { it('gets the corners and center', () => { const cornersAndCenter = box.cornersAndCenter expect(cornersAndCenter).toEqual([ new Vec(10, 20), // top-left new Vec(110, 20), // top-right (fixed from buggy bottom-right) new Vec(110, 220), // bottom-right new Vec(10, 220), // bottom-left new Vec(60, 120), // center ]) }) }) describe('Box.sides', () => { it('gets the sides as pairs of vectors', () => { const sides = box.sides const corners = box.corners expect(sides).toEqual([ [corners[0], corners[1]], [corners[1], corners[2]], [corners[2], corners[3]], [corners[3], corners[0]], ]) }) }) describe('Box.size', () => { it('gets the size as a Vec', () => { expect(box.size).toEqual(new Vec(100, 200)) }) }) describe('Box.toFixed', () => { it('applies precision to all values', () => { const impreciseBox = new Box(10.123456789, 20.987654321, 100.555555, 200.777777) const result = impreciseBox.toFixed() expect(result.x).toBeCloseTo(10.123456789, 6) expect(result.y).toBeCloseTo(20.987654321, 6) expect(result.w).toBeCloseTo(100.555555, 6) expect(result.h).toBeCloseTo(200.777777, 6) expect(result).toBe(impreciseBox) // returns self }) }) describe('Box.setTo', () => { it('copies values from another box', () => { const otherBox = new Box(50, 60, 150, 250) const result = box.setTo(otherBox) expect(box.x).toBe(50) expect(box.y).toBe(60) expect(box.w).toBe(150) expect(box.h).toBe(250) expect(result).toBe(box) // returns self }) }) describe('Box.set', () => { it('sets all values', () => { const result = box.set(30, 40, 120, 180) expect(box.x).toBe(30) expect(box.y).toBe(40) expect(box.w).toBe(120) expect(box.h).toBe(180) expect(result).toBe(box) // returns self }) it('uses default values when no parameters provided', () => { const result = box.set() expect(box.x).toBe(0) expect(box.y).toBe(0) expect(box.w).toBe(0) expect(box.h).toBe(0) expect(result).toBe(box) // returns self }) }) describe('Box.expand', () => { it('expands to include another box', () => { const otherBox = new Box(5, 15, 120, 240) // overlaps and extends const result = box.expand(otherBox) expect(box.x).toBe(5) // min of 10 and 5 expect(box.y).toBe(15) // min of 20 and 15 expect(box.w).toBe(120) // max(110, 125) - min(10, 5) = 125 - 5 expect(box.h).toBe(240) // max(220, 255) - min(20, 15) = 255 - 15 expect(result).toBe(box) // returns self }) }) describe('Box.expandBy', () => { it('expands by a given amount', () => { const result = box.expandBy(10) expect(box.x).toBe(0) // 10 - 10 expect(box.y).toBe(10) // 20 - 10 expect(box.w).toBe(120) // 100 + 10*2 expect(box.h).toBe(220) // 200 + 10*2 expect(result).toBe(box) // returns self }) }) describe('Box.scale', () => { it('scales the box by a factor', () => { const result = box.scale(2) expect(box.x).toBe(5) // 10/2 expect(box.y).toBe(10) // 20/2 expect(box.w).toBe(50) // 100/2 expect(box.h).toBe(100) // 200/2 expect(result).toBe(box) // returns self }) }) describe('Box.clone', () => { it('creates a copy of the box', () => { const cloned = box.clone() expect(cloned).toEqual(box) expect(cloned).not.toBe(box) // different object }) }) describe('Box.translate', () => { it('translates the box by a delta', () => { const delta = new Vec(5, 10) const result = box.translate(delta) expect(box.x).toBe(15) // 10 + 5 expect(box.y).toBe(30) // 20 + 10 expect(box.w).toBe(100) // unchanged expect(box.h).toBe(200) // unchanged expect(result).toBe(box) // returns self }) }) describe('Box.snapToGrid', () => { it('snaps the box to a grid', () => { const testBox = new Box(12, 18, 95, 185) testBox.snapToGrid(10) expect(testBox.minX).toBe(10) // rounded to nearest 10 expect(testBox.minY).toBe(20) // rounded to nearest 10 expect(testBox.width).toBe(100) // snapped width expect(testBox.height).toBe(180) // snapped height }) it('ensures minimum size of 1', () => { const testBox = new Box(0, 0, 3, 3) testBox.snapToGrid(10) expect(testBox.width).toBe(1) // minimum 1 expect(testBox.height).toBe(1) // minimum 1 }) }) describe('Box.collides', () => { it('returns true when boxes collide', () => { const otherBox = new Box(50, 100, 100, 100) // overlaps expect(box.collides(otherBox)).toBe(true) }) it('returns false when boxes do not collide', () => { const otherBox = new Box(200, 300, 100, 100) // no overlap expect(box.collides(otherBox)).toBe(false) }) }) describe('Box.contains', () => { it('returns true when this box contains the other', () => { const otherBox = new Box(20, 30, 50, 100) // inside box expect(box.contains(otherBox)).toBe(true) }) it('returns false when this box does not contain the other', () => { const otherBox = new Box(5, 5, 200, 300) // larger than box expect(box.contains(otherBox)).toBe(false) }) }) describe('Box.includes', () => { it('returns true when boxes collide or contain', () => { const collidingBox = new Box(50, 100, 100, 100) const containedBox = new Box(20, 30, 50, 100) expect(box.includes(collidingBox)).toBe(true) expect(box.includes(containedBox)).toBe(true) }) it('returns false when boxes do not interact', () => { const separateBox = new Box(200, 300, 100, 100) expect(box.includes(separateBox)).toBe(false) }) }) describe('Box.containsPoint', () => { it('returns true when point is inside', () => { const point = new Vec(50, 100) expect(box.containsPoint(point)).toBe(true) }) it('returns false when point is outside', () => { const point = new Vec(150, 300) expect(box.containsPoint(point)).toBe(false) }) it('respects margin', () => { const point = new Vec(5, 15) // just outside expect(box.containsPoint(point)).toBe(false) expect(box.containsPoint(point, 10)).toBe(true) // with margin }) }) describe('Box.getHandlePoint', () => { it('returns correct points for corners', () => { expect(box.getHandlePoint('top_left')).toEqual(new Vec(10, 20)) expect(box.getHandlePoint('top_right')).toEqual(new Vec(110, 20)) expect(box.getHandlePoint('bottom_left')).toEqual(new Vec(10, 220)) expect(box.getHandlePoint('bottom_right')).toEqual(new Vec(110, 220)) }) it('returns correct points for edges', () => { expect(box.getHandlePoint('top')).toEqual(new Vec(60, 20)) expect(box.getHandlePoint('right')).toEqual(new Vec(110, 120)) expect(box.getHandlePoint('bottom')).toEqual(new Vec(60, 220)) expect(box.getHandlePoint('left')).toEqual(new Vec(10, 120)) }) }) describe('Box.toJson', () => { it('returns box model object', () => { expect(box.toJson()).toEqual({ x: 10, y: 20, w: 100, h: 200, }) }) }) describe('Box.resize', () => { it('resizes from top-left handle', () => { box.resize('top_left', 10, 20) expect(box.minX).toBe(20) // moved right expect(box.minY).toBe(40) // moved down expect(box.width).toBe(90) // reduced width expect(box.height).toBe(180) // reduced height }) it('resizes from bottom-right handle', () => { box.resize('bottom_right', 10, 20) expect(box.minX).toBe(10) // unchanged expect(box.minY).toBe(20) // unchanged expect(box.width).toBe(110) // increased width expect(box.height).toBe(220) // increased height }) }) describe('Box.union', () => { it('creates union with another box', () => { const otherBox = { x: 5, y: 15, w: 120, h: 240 } const result = box.union(otherBox) expect(box.x).toBe(5) expect(box.y).toBe(15) expect(box.width).toBe(120) // max(110, 125) - min(10, 5) expect(box.height).toBe(240) // max(220, 255) - min(20, 15) expect(result).toBe(box) // returns self }) }) describe('Box.equals', () => { it('returns true for equal boxes', () => { const otherBox = new Box(10, 20, 100, 200) expect(box.equals(otherBox)).toBe(true) }) it('returns false for different boxes', () => { const otherBox = new Box(10, 20, 100, 201) expect(box.equals(otherBox)).toBe(false) }) }) describe('Box.zeroFix', () => { it('ensures minimum size of 1', () => { const zeroBox = new Box(0, 0, 0, 0) const result = zeroBox.zeroFix() expect(zeroBox.w).toBe(1) expect(zeroBox.h).toBe(1) expect(result).toBe(zeroBox) // returns self }) }) // Static method tests describe('Box.From', () => { it('creates box from box model', () => { const boxModel = { x: 5, y: 10, w: 50, h: 100 } const result = Box.From(boxModel) expect(result).toEqual(new Box(5, 10, 50, 100)) }) }) describe('Box.FromCenter', () => { it('creates box from center and size', () => { const center = new Vec(50, 100) const size = new Vec(20, 40) const result = Box.FromCenter(center, size) expect(result).toEqual(new Box(40, 80, 20, 40)) }) }) describe('Box.FromPoints', () => { it('creates box from array of points', () => { const points = [new Vec(10, 20), new Vec(110, 220), new Vec(50, 100)] const result = Box.FromPoints(points) expect(result).toEqual(new Box(10, 20, 100, 200)) }) it('returns empty box for empty array', () => { const result = Box.FromPoints([]) expect(result).toEqual(new Box()) }) }) describe('Box.Expand', () => { it('creates expanded box from two boxes', () => { const boxA = new Box(10, 20, 100, 200) const boxB = new Box(5, 15, 120, 240) const result = Box.Expand(boxA, boxB) expect(result).toEqual(new Box(5, 15, 120, 240)) }) }) describe('Box.ExpandBy', () => { it('creates expanded box by amount', () => { const result = Box.ExpandBy(box, 10) expect(result).toEqual(new Box(0, 10, 120, 220)) }) }) describe('Box.Collides', () => { it('returns true when boxes collide', () => { const boxA = new Box(0, 0, 50, 50) const boxB = new Box(25, 25, 50, 50) expect(Box.Collides(boxA, boxB)).toBe(true) }) it('returns false when boxes do not collide', () => { const boxA = new Box(0, 0, 50, 50) const boxB = new Box(100, 100, 50, 50) expect(Box.Collides(boxA, boxB)).toBe(false) }) }) describe('Box.Contains', () => { it('returns true when first box contains second', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(10, 10, 50, 50) expect(Box.Contains(boxA, boxB)).toBe(true) }) it('returns false when first box does not contain second', () => { const boxA = new Box(0, 0, 50, 50) const boxB = new Box(10, 10, 100, 100) expect(Box.Contains(boxA, boxB)).toBe(false) }) }) describe('Box.ContainsApproximately', () => { it('returns true when first box exactly contains second', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(10, 10, 50, 50) expect(Box.ContainsApproximately(boxA, boxB)).toBe(true) }) it('returns false when first box clearly does not contain second', () => { const boxA = new Box(0, 0, 50, 50) const boxB = new Box(10, 10, 100, 100) expect(Box.ContainsApproximately(boxA, boxB)).toBe(false) }) it('returns true when containment is within default precision tolerance', () => { // Box B extends very slightly outside A (within floating-point precision) const boxA = new Box(0, 0, 100, 100) const boxB = new Box(10, 10, 80, 80) // Move B's max edges just slightly outside A's bounds boxB.w = 90.000000000001 // maxX = 100.000000000001 (slightly beyond 100) boxB.h = 90.000000000001 // maxY = 100.000000000001 (slightly beyond 100) expect(Box.ContainsApproximately(boxA, boxB)).toBe(true) expect(Box.Contains(boxA, boxB)).toBe(false) // strict contains would fail }) it('returns false when containment exceeds default precision tolerance', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(10, 10, 80, 80) // Move B's max edges clearly outside A's bounds boxB.w = 95 // maxX = 105 (clearly beyond 100) boxB.h = 95 // maxY = 105 (clearly beyond 100) expect(Box.ContainsApproximately(boxA, boxB)).toBe(false) }) it('respects custom precision parameter', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(10, 10, 85, 85) // maxX=95, maxY=95 // With loose precision (10), should contain (95 is within 100-10=90 tolerance) expect(Box.ContainsApproximately(boxA, boxB, 10)).toBe(true) // With tight precision (4), should still contain (95 is within 100-4=96) expect(Box.ContainsApproximately(boxA, boxB, 4)).toBe(true) // Since 95 < 100, the precision parameter doesn't affect containment here expect(Box.ContainsApproximately(boxA, boxB, 4.9)).toBe(true) }) it('handles negative coordinates correctly', () => { const boxA = new Box(-50, -50, 100, 100) // bounds: (-50,-50) to (50,50) const boxB = new Box(-40, -40, 79.999999999, 79.999999999) // bounds: (-40,-40) to (39.999999999, 39.999999999) expect(Box.ContainsApproximately(boxA, boxB)).toBe(true) }) it('handles edge case where boxes are identical', () => { const boxA = new Box(10, 20, 100, 200) const boxB = new Box(10, 20, 100, 200) expect(Box.ContainsApproximately(boxA, boxB)).toBe(true) }) it('handles edge case where inner box touches outer box edges', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(0, 0, 100, 100) // exactly the same expect(Box.ContainsApproximately(boxA, boxB)).toBe(true) // Slightly smaller inner box const boxC = new Box(0.000001, 0.000001, 99.999998, 99.999998) expect(Box.ContainsApproximately(boxA, boxC)).toBe(true) }) it('handles floating-point precision issues in real-world scenarios', () => { // Simulate common floating-point arithmetic issues const containerBox = new Box(0, 0, 100, 100) // Box that should be contained but has floating-point errors const innerBox = new Box(10, 10, 80, 80) // Simulate floating-point arithmetic that results in tiny overruns innerBox.w = 90.00000000000001 // maxX = 100.00000000000001 (tiny overrun) innerBox.h = 90.00000000000001 // maxY = 100.00000000000001 (tiny overrun) expect(Box.ContainsApproximately(containerBox, innerBox)).toBe(true) expect(Box.Contains(containerBox, innerBox)).toBe(false) // strict contains fails due to precision }) it('fails when any edge exceeds tolerance', () => { const boxA = new Box(10, 10, 100, 100) // bounds: (10,10) to (110,110) // Test each edge exceeding tolerance const testCases = [ { name: 'left edge', box: new Box(5, 20, 80, 80) }, // minX too small { name: 'top edge', box: new Box(20, 5, 80, 80) }, // minY too small { name: 'right edge', box: new Box(20, 20, 95, 80) }, // maxX too large (20+95=115 > 110) { name: 'bottom edge', box: new Box(20, 20, 80, 95) }, // maxY too large (20+95=115 > 110) ] testCases.forEach(({ box }) => { expect(Box.ContainsApproximately(boxA, box, 1)).toBe(false) // tight precision }) }) it('works with zero-sized dimensions', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(50, 50, 0, 0) // zero-sized box (point) expect(Box.ContainsApproximately(boxA, boxB)).toBe(true) }) it('handles precision parameter edge cases', () => { const boxA = new Box(0, 0, 100, 100) const boxB = new Box(10, 10, 91, 91) // maxX=101, maxY=101 (clearly outside) // Zero precision should work like strict Contains expect(Box.ContainsApproximately(boxA, boxB, 0)).toBe(false) // Small precision should still fail (101 > 100) expect(Box.ContainsApproximately(boxA, boxB, 0.5)).toBe(false) // Sufficient precision should succeed (101 <= 100 + 2) expect(Box.ContainsApproximately(boxA, boxB, 2)).toBe(true) }) }) describe('Box.Includes', () => { it('returns true when boxes collide or contain', () => { const boxA = new Box(0, 0, 50, 50) const boxB = new Box(25, 25, 50, 50) // colliding const boxC = new Box(10, 10, 20, 20) // contained expect(Box.Includes(boxA, boxB)).toBe(true) expect(Box.Includes(boxA, boxC)).toBe(true) }) it('returns false when boxes do not interact', () => { const boxA = new Box(0, 0, 50, 50) const boxB = new Box(100, 100, 50, 50) expect(Box.Includes(boxA, boxB)).toBe(false) }) }) describe('Box.ContainsPoint', () => { it('returns true when point is inside box', () => { const testBox = new Box(0, 0, 100, 100) const point = new Vec(50, 50) expect(Box.ContainsPoint(testBox, point)).toBe(true) }) it('returns false when point is outside box', () => { const testBox = new Box(0, 0, 100, 100) const point = new Vec(150, 150) expect(Box.ContainsPoint(testBox, point)).toBe(false) }) it('respects margin parameter', () => { const testBox = new Box(0, 0, 100, 100) const point = new Vec(-5, -5) expect(Box.ContainsPoint(testBox, point)).toBe(false) expect(Box.ContainsPoint(testBox, point, 10)).toBe(true) }) }) describe('Box.Common', () => { it('creates bounding box of multiple boxes', () => { const boxes = [new Box(0, 0, 50, 50), new Box(75, 75, 50, 50), new Box(25, 25, 50, 50)] const result = Box.Common(boxes) expect(result).toEqual(new Box(0, 0, 125, 125)) }) }) describe('Box.Sides', () => { it('returns sides as corner pairs', () => { const testBox = new Box(0, 0, 100, 100) const sides = Box.Sides(testBox) expect(sides).toHaveLength(4) expect(sides[0]).toEqual([new Vec(0, 0), new Vec(100, 0)]) }) }) describe('Box.Resize', () => { it('resizes box and returns scaling info', () => { const testBox = new Box(0, 0, 100, 100) const result = Box.Resize(testBox, 'bottom_right', 50, 50) expect(result.box).toEqual(new Box(0, 0, 150, 150)) expect(result.scaleX).toBe(1.5) expect(result.scaleY).toBe(1.5) }) it('handles aspect ratio locking', () => { const testBox = new Box(0, 0, 100, 100) const result = Box.Resize(testBox, 'bottom_right', 50, 25, true) expect(result.box.width).toBeCloseTo(result.box.height) // maintains aspect ratio }) }) describe('Box.Equals', () => { it('returns true for equal boxes', () => { const boxA = new Box(10, 20, 100, 200) const boxB = new Box(10, 20, 100, 200) expect(Box.Equals(boxA, boxB)).toBe(true) }) it('returns false for different boxes', () => { const boxA = new Box(10, 20, 100, 200) const boxB = new Box(10, 20, 100, 201) expect(Box.Equals(boxA, boxB)).toBe(false) }) }) describe('Box.ZeroFix', () => { it('creates new box with minimum size of 1', () => { const zeroBox = new Box(0, 0, 0, 0) const result = Box.ZeroFix(zeroBox) expect(result).toEqual(new Box(0, 0, 1, 1)) expect(result).not.toBe(zeroBox) // different object }) }) })