test(engine): split geometry coverage by domain
This commit is contained in:
parent
ab700fd8be
commit
e6a6926f39
|
|
@ -272,6 +272,11 @@ const ENGINE_TEST_DOMAIN_REDIRECTS: Array<{
|
|||
from: 'tests/engine/vector/basic',
|
||||
to: 'tests/engine/vector/blob/, tests/engine/vector/normalize.test.ts, tests/engine/vector/validate.test.ts, or tests/engine/vector/bounds.test.ts',
|
||||
source: 'the vector module under test'
|
||||
},
|
||||
{
|
||||
from: 'tests/engine/geometry/basic',
|
||||
to: 'tests/engine/geometry/angles.test.ts, tests/engine/geometry/rotation.test.ts, tests/engine/geometry/bounds.test.ts, or tests/engine/geometry/visual-bounds.test.ts',
|
||||
source: 'the geometry module under test'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
24
tests/engine/geometry/angles.test.ts
Normal file
24
tests/engine/geometry/angles.test.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, test, expect } from 'bun:test'
|
||||
|
||||
import { degToRad, radToDeg } from '@open-pencil/core'
|
||||
|
||||
describe('degToRad / radToDeg', () => {
|
||||
test('degToRad converts degrees to radians', () => {
|
||||
expect(degToRad(0)).toBe(0)
|
||||
expect(degToRad(90)).toBeCloseTo(Math.PI / 2, 10)
|
||||
expect(degToRad(180)).toBeCloseTo(Math.PI, 10)
|
||||
expect(degToRad(360)).toBeCloseTo(Math.PI * 2, 10)
|
||||
})
|
||||
|
||||
test('radToDeg converts radians to degrees', () => {
|
||||
expect(radToDeg(0)).toBe(0)
|
||||
expect(radToDeg(Math.PI / 2)).toBeCloseTo(90, 10)
|
||||
expect(radToDeg(Math.PI)).toBeCloseTo(180, 10)
|
||||
})
|
||||
|
||||
test('roundtrip preserves value', () => {
|
||||
expect(radToDeg(degToRad(45))).toBeCloseTo(45, 10)
|
||||
expect(radToDeg(degToRad(-90))).toBeCloseTo(-90, 10)
|
||||
})
|
||||
})
|
||||
|
||||
62
tests/engine/geometry/bounds.test.ts
Normal file
62
tests/engine/geometry/bounds.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, test, expect } from 'bun:test'
|
||||
|
||||
import { computeAbsoluteBounds, computeBounds, type Vector } from '@open-pencil/core'
|
||||
|
||||
describe('computeBounds', () => {
|
||||
test('empty iterable returns zero rect', () => {
|
||||
expect(computeBounds([])).toEqual({ x: 0, y: 0, width: 0, height: 0 })
|
||||
})
|
||||
|
||||
test('single rect returns itself', () => {
|
||||
const r = { x: 10, y: 20, width: 30, height: 40 }
|
||||
expect(computeBounds([r])).toEqual(r)
|
||||
})
|
||||
|
||||
test('multiple rects returns union', () => {
|
||||
const result = computeBounds([
|
||||
{ x: 0, y: 0, width: 100, height: 100 },
|
||||
{ x: 50, y: 50, width: 100, height: 100 }
|
||||
])
|
||||
expect(result).toEqual({ x: 0, y: 0, width: 150, height: 150 })
|
||||
})
|
||||
|
||||
test('disjoint rects returns union spanning both', () => {
|
||||
const result = computeBounds([
|
||||
{ x: 0, y: 0, width: 10, height: 10 },
|
||||
{ x: 100, y: 100, width: 10, height: 10 }
|
||||
])
|
||||
expect(result).toEqual({ x: 0, y: 0, width: 110, height: 110 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeAbsoluteBounds', () => {
|
||||
const idPos = (id: string) => {
|
||||
const map: Record<string, Vector> = {
|
||||
a: { x: 10, y: 20 },
|
||||
b: { x: 50, y: 60 },
|
||||
c: { x: 0, y: 0 }
|
||||
}
|
||||
return map[id] ?? { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
test('empty iterable returns zero rect', () => {
|
||||
expect(computeAbsoluteBounds([], idPos)).toEqual({ x: 0, y: 0, width: 0, height: 0 })
|
||||
})
|
||||
|
||||
test('single node returns its bounds', () => {
|
||||
const result = computeAbsoluteBounds([{ id: 'a', width: 30, height: 40 }], idPos)
|
||||
expect(result).toEqual({ x: 10, y: 20, width: 30, height: 40 })
|
||||
})
|
||||
|
||||
test('multiple nodes returns union', () => {
|
||||
const result = computeAbsoluteBounds(
|
||||
[
|
||||
{ id: 'a', width: 30, height: 40 },
|
||||
{ id: 'b', width: 20, height: 20 }
|
||||
],
|
||||
idPos
|
||||
)
|
||||
expect(result).toEqual({ x: 10, y: 20, width: 60, height: 60 })
|
||||
})
|
||||
})
|
||||
|
||||
95
tests/engine/geometry/rotation.test.ts
Normal file
95
tests/engine/geometry/rotation.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { describe, test, expect } from 'bun:test'
|
||||
|
||||
import { rotatePoint, rotatedBBox, rotatedCorners } from '@open-pencil/core'
|
||||
|
||||
describe('rotatePoint', () => {
|
||||
test('no rotation returns same point', () => {
|
||||
const result = rotatePoint(10, 20, 0, 0, 0)
|
||||
expect(result.x).toBeCloseTo(10, 10)
|
||||
expect(result.y).toBeCloseTo(20, 10)
|
||||
})
|
||||
|
||||
test('90° rotation around origin', () => {
|
||||
const result = rotatePoint(1, 0, 0, 0, Math.PI / 2)
|
||||
expect(result.x).toBeCloseTo(0, 10)
|
||||
expect(result.y).toBeCloseTo(1, 10)
|
||||
})
|
||||
|
||||
test('180° rotation negates both axes', () => {
|
||||
const result = rotatePoint(1, 0, 0, 0, Math.PI)
|
||||
expect(result.x).toBeCloseTo(-1, 10)
|
||||
expect(result.y).toBeCloseTo(0, 10)
|
||||
})
|
||||
|
||||
test('rotation around non-origin center', () => {
|
||||
// Rotate (1,0) around (0.5, 0.5) by 90° → (1, 1)
|
||||
const result = rotatePoint(1, 0, 0.5, 0.5, Math.PI / 2)
|
||||
expect(result.x).toBeCloseTo(1, 10)
|
||||
expect(result.y).toBeCloseTo(1, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotatedCorners', () => {
|
||||
test('zero rotation returns axis-aligned corners', () => {
|
||||
const [tl, tr, br, bl] = rotatedCorners(50, 50, 50, 50, 0)
|
||||
expect(tl).toEqual({ x: 0, y: 0 })
|
||||
expect(tr).toEqual({ x: 100, y: 0 })
|
||||
expect(br).toEqual({ x: 100, y: 100 })
|
||||
expect(bl).toEqual({ x: 0, y: 100 })
|
||||
})
|
||||
|
||||
test('90° rotation swaps width and height positions', () => {
|
||||
const [tl, tr, br, bl] = rotatedCorners(50, 50, 50, 25, 90)
|
||||
// After 90° rotation: corners should be at different positions
|
||||
// The center (50,50) stays the same
|
||||
const cx = (tl.x + tr.x + br.x + bl.x) / 4
|
||||
const cy = (tl.y + tr.y + br.y + bl.y) / 4
|
||||
expect(cx).toBeCloseTo(50, 10)
|
||||
expect(cy).toBeCloseTo(50, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotatedBBox', () => {
|
||||
test('zero rotation returns input as bbox', () => {
|
||||
const bbox = rotatedBBox(10, 20, 30, 40, 0)
|
||||
expect(bbox).toEqual({
|
||||
left: 10,
|
||||
right: 40,
|
||||
top: 20,
|
||||
bottom: 60,
|
||||
centerX: 25,
|
||||
centerY: 40
|
||||
})
|
||||
})
|
||||
|
||||
test('45° rotation of square expands bbox', () => {
|
||||
const bbox = rotatedBBox(0, 0, 100, 100, 45)
|
||||
// A 100x100 square rotated 45° has a bbox of ~141.4 wide/tall
|
||||
const side = Math.sqrt(100 * 100 * 2)
|
||||
expect(bbox.right - bbox.left).toBeCloseTo(side, 5)
|
||||
expect(bbox.bottom - bbox.top).toBeCloseTo(side, 5)
|
||||
// Center should remain at (50, 50)
|
||||
expect(bbox.centerX).toBeCloseTo(50, 5)
|
||||
expect(bbox.centerY).toBeCloseTo(50, 5)
|
||||
})
|
||||
|
||||
test('90° rotation of rectangle swaps dimensions', () => {
|
||||
const bbox = rotatedBBox(0, 0, 200, 100, 90)
|
||||
// A 200x100 rect rotated 90° around its center (100, 50) → bbox 100x200
|
||||
// The center is at (100, 50) regardless of rotation
|
||||
expect(bbox.centerX).toBeCloseTo(100, 5)
|
||||
expect(bbox.centerY).toBeCloseTo(50, 5)
|
||||
// Width of bbox = original height, height of bbox = original width
|
||||
expect(bbox.right - bbox.left).toBeCloseTo(100, 5)
|
||||
expect(bbox.bottom - bbox.top).toBeCloseTo(200, 5)
|
||||
})
|
||||
|
||||
test('negative rotation works correctly', () => {
|
||||
const pos = rotatedBBox(0, 0, 100, 100, -45)
|
||||
const neg = rotatedBBox(0, 0, 100, 100, 45)
|
||||
// Bounding box should be same size regardless of rotation direction
|
||||
expect(pos.right - pos.left).toBeCloseTo(neg.right - neg.left, 5)
|
||||
expect(pos.bottom - pos.top).toBeCloseTo(neg.bottom - neg.top, 5)
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -1,194 +1,9 @@
|
|||
/**
|
||||
* Tests for geometry utility functions in packages/core/src/geometry.ts.
|
||||
*
|
||||
* These pure functions compute bounding boxes, rotated corners, and visual bounds
|
||||
* for scene nodes. They are used by the renderer for culling, export bounds
|
||||
* computation, and hit testing.
|
||||
*/
|
||||
import { describe, test, expect } from 'bun:test'
|
||||
|
||||
import {
|
||||
computeBounds,
|
||||
computeAbsoluteBounds,
|
||||
degToRad,
|
||||
radToDeg,
|
||||
rotatePoint,
|
||||
rotatedCorners,
|
||||
rotatedBBox,
|
||||
type Vector
|
||||
} from '@open-pencil/core'
|
||||
import type { Vector } from '@open-pencil/core'
|
||||
|
||||
import { computeVisualBounds } from '#core/geometry'
|
||||
|
||||
describe('degToRad / radToDeg', () => {
|
||||
test('degToRad converts degrees to radians', () => {
|
||||
expect(degToRad(0)).toBe(0)
|
||||
expect(degToRad(90)).toBeCloseTo(Math.PI / 2, 10)
|
||||
expect(degToRad(180)).toBeCloseTo(Math.PI, 10)
|
||||
expect(degToRad(360)).toBeCloseTo(Math.PI * 2, 10)
|
||||
})
|
||||
|
||||
test('radToDeg converts radians to degrees', () => {
|
||||
expect(radToDeg(0)).toBe(0)
|
||||
expect(radToDeg(Math.PI / 2)).toBeCloseTo(90, 10)
|
||||
expect(radToDeg(Math.PI)).toBeCloseTo(180, 10)
|
||||
})
|
||||
|
||||
test('roundtrip preserves value', () => {
|
||||
expect(radToDeg(degToRad(45))).toBeCloseTo(45, 10)
|
||||
expect(radToDeg(degToRad(-90))).toBeCloseTo(-90, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotatePoint', () => {
|
||||
test('no rotation returns same point', () => {
|
||||
const result = rotatePoint(10, 20, 0, 0, 0)
|
||||
expect(result.x).toBeCloseTo(10, 10)
|
||||
expect(result.y).toBeCloseTo(20, 10)
|
||||
})
|
||||
|
||||
test('90° rotation around origin', () => {
|
||||
const result = rotatePoint(1, 0, 0, 0, Math.PI / 2)
|
||||
expect(result.x).toBeCloseTo(0, 10)
|
||||
expect(result.y).toBeCloseTo(1, 10)
|
||||
})
|
||||
|
||||
test('180° rotation negates both axes', () => {
|
||||
const result = rotatePoint(1, 0, 0, 0, Math.PI)
|
||||
expect(result.x).toBeCloseTo(-1, 10)
|
||||
expect(result.y).toBeCloseTo(0, 10)
|
||||
})
|
||||
|
||||
test('rotation around non-origin center', () => {
|
||||
// Rotate (1,0) around (0.5, 0.5) by 90° → (1, 1)
|
||||
const result = rotatePoint(1, 0, 0.5, 0.5, Math.PI / 2)
|
||||
expect(result.x).toBeCloseTo(1, 10)
|
||||
expect(result.y).toBeCloseTo(1, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotatedCorners', () => {
|
||||
test('zero rotation returns axis-aligned corners', () => {
|
||||
const [tl, tr, br, bl] = rotatedCorners(50, 50, 50, 50, 0)
|
||||
expect(tl).toEqual({ x: 0, y: 0 })
|
||||
expect(tr).toEqual({ x: 100, y: 0 })
|
||||
expect(br).toEqual({ x: 100, y: 100 })
|
||||
expect(bl).toEqual({ x: 0, y: 100 })
|
||||
})
|
||||
|
||||
test('90° rotation swaps width and height positions', () => {
|
||||
const [tl, tr, br, bl] = rotatedCorners(50, 50, 50, 25, 90)
|
||||
// After 90° rotation: corners should be at different positions
|
||||
// The center (50,50) stays the same
|
||||
const cx = (tl.x + tr.x + br.x + bl.x) / 4
|
||||
const cy = (tl.y + tr.y + br.y + bl.y) / 4
|
||||
expect(cx).toBeCloseTo(50, 10)
|
||||
expect(cy).toBeCloseTo(50, 10)
|
||||
})
|
||||
})
|
||||
|
||||
describe('rotatedBBox', () => {
|
||||
test('zero rotation returns input as bbox', () => {
|
||||
const bbox = rotatedBBox(10, 20, 30, 40, 0)
|
||||
expect(bbox).toEqual({
|
||||
left: 10,
|
||||
right: 40,
|
||||
top: 20,
|
||||
bottom: 60,
|
||||
centerX: 25,
|
||||
centerY: 40
|
||||
})
|
||||
})
|
||||
|
||||
test('45° rotation of square expands bbox', () => {
|
||||
const bbox = rotatedBBox(0, 0, 100, 100, 45)
|
||||
// A 100x100 square rotated 45° has a bbox of ~141.4 wide/tall
|
||||
const side = Math.sqrt(100 * 100 * 2)
|
||||
expect(bbox.right - bbox.left).toBeCloseTo(side, 5)
|
||||
expect(bbox.bottom - bbox.top).toBeCloseTo(side, 5)
|
||||
// Center should remain at (50, 50)
|
||||
expect(bbox.centerX).toBeCloseTo(50, 5)
|
||||
expect(bbox.centerY).toBeCloseTo(50, 5)
|
||||
})
|
||||
|
||||
test('90° rotation of rectangle swaps dimensions', () => {
|
||||
const bbox = rotatedBBox(0, 0, 200, 100, 90)
|
||||
// A 200x100 rect rotated 90° around its center (100, 50) → bbox 100x200
|
||||
// The center is at (100, 50) regardless of rotation
|
||||
expect(bbox.centerX).toBeCloseTo(100, 5)
|
||||
expect(bbox.centerY).toBeCloseTo(50, 5)
|
||||
// Width of bbox = original height, height of bbox = original width
|
||||
expect(bbox.right - bbox.left).toBeCloseTo(100, 5)
|
||||
expect(bbox.bottom - bbox.top).toBeCloseTo(200, 5)
|
||||
})
|
||||
|
||||
test('negative rotation works correctly', () => {
|
||||
const pos = rotatedBBox(0, 0, 100, 100, -45)
|
||||
const neg = rotatedBBox(0, 0, 100, 100, 45)
|
||||
// Bounding box should be same size regardless of rotation direction
|
||||
expect(pos.right - pos.left).toBeCloseTo(neg.right - neg.left, 5)
|
||||
expect(pos.bottom - pos.top).toBeCloseTo(neg.bottom - neg.top, 5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeBounds', () => {
|
||||
test('empty iterable returns zero rect', () => {
|
||||
expect(computeBounds([])).toEqual({ x: 0, y: 0, width: 0, height: 0 })
|
||||
})
|
||||
|
||||
test('single rect returns itself', () => {
|
||||
const r = { x: 10, y: 20, width: 30, height: 40 }
|
||||
expect(computeBounds([r])).toEqual(r)
|
||||
})
|
||||
|
||||
test('multiple rects returns union', () => {
|
||||
const result = computeBounds([
|
||||
{ x: 0, y: 0, width: 100, height: 100 },
|
||||
{ x: 50, y: 50, width: 100, height: 100 }
|
||||
])
|
||||
expect(result).toEqual({ x: 0, y: 0, width: 150, height: 150 })
|
||||
})
|
||||
|
||||
test('disjoint rects returns union spanning both', () => {
|
||||
const result = computeBounds([
|
||||
{ x: 0, y: 0, width: 10, height: 10 },
|
||||
{ x: 100, y: 100, width: 10, height: 10 }
|
||||
])
|
||||
expect(result).toEqual({ x: 0, y: 0, width: 110, height: 110 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeAbsoluteBounds', () => {
|
||||
const idPos = (id: string) => {
|
||||
const map: Record<string, Vector> = {
|
||||
a: { x: 10, y: 20 },
|
||||
b: { x: 50, y: 60 },
|
||||
c: { x: 0, y: 0 }
|
||||
}
|
||||
return map[id] ?? { x: 0, y: 0 }
|
||||
}
|
||||
|
||||
test('empty iterable returns zero rect', () => {
|
||||
expect(computeAbsoluteBounds([], idPos)).toEqual({ x: 0, y: 0, width: 0, height: 0 })
|
||||
})
|
||||
|
||||
test('single node returns its bounds', () => {
|
||||
const result = computeAbsoluteBounds([{ id: 'a', width: 30, height: 40 }], idPos)
|
||||
expect(result).toEqual({ x: 10, y: 20, width: 30, height: 40 })
|
||||
})
|
||||
|
||||
test('multiple nodes returns union', () => {
|
||||
const result = computeAbsoluteBounds(
|
||||
[
|
||||
{ id: 'a', width: 30, height: 40 },
|
||||
{ id: 'b', width: 20, height: 20 }
|
||||
],
|
||||
idPos
|
||||
)
|
||||
expect(result).toEqual({ x: 10, y: 20, width: 60, height: 60 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('computeVisualBounds', () => {
|
||||
const idPos = (id: string) => {
|
||||
const map: Record<string, Vector> = {
|
||||
Loading…
Reference in a new issue