162 lines
4.7 KiB
TypeScript
162 lines
4.7 KiB
TypeScript
import { describe, test, expect } from 'bun:test'
|
|
|
|
import type { Vector } from '@open-pencil/core'
|
|
import { fitCircleArc, isClosedThinCrescent } from '@open-pencil/core/vector'
|
|
import type { VectorNetwork } from '@open-pencil/scene-graph'
|
|
|
|
import { expectDefined } from '#tests/helpers/assert'
|
|
|
|
function makeAnnularWedge(
|
|
cx: number,
|
|
cy: number,
|
|
rInner: number,
|
|
rOuter: number,
|
|
startDeg: number,
|
|
sweepDeg: number,
|
|
segmentsPerArc: number
|
|
): VectorNetwork {
|
|
const vertices: Vector[] = []
|
|
const segments: VectorNetwork['segments'] = []
|
|
|
|
for (let i = 0; i <= segmentsPerArc; i++) {
|
|
const angle = ((startDeg + (sweepDeg * i) / segmentsPerArc) * Math.PI) / 180
|
|
vertices.push({ x: cx + rOuter * Math.cos(angle), y: cy + rOuter * Math.sin(angle) })
|
|
}
|
|
for (let i = segmentsPerArc; i >= 0; i--) {
|
|
const angle = ((startDeg + (sweepDeg * i) / segmentsPerArc) * Math.PI) / 180
|
|
vertices.push({ x: cx + rInner * Math.cos(angle), y: cy + rInner * Math.sin(angle) })
|
|
}
|
|
|
|
const n = vertices.length
|
|
for (let i = 0; i < n; i++) {
|
|
segments.push({
|
|
start: i,
|
|
end: (i + 1) % n,
|
|
tangentStart: { x: 0, y: 0 },
|
|
tangentEnd: { x: 0, y: 0 }
|
|
})
|
|
}
|
|
|
|
return { vertices, segments, regions: [] }
|
|
}
|
|
|
|
describe('fitCircleArc', () => {
|
|
test('returns null for fewer than 3 points', () => {
|
|
expect(fitCircleArc([])).toBeNull()
|
|
expect(fitCircleArc([{ x: 0, y: 0 }])).toBeNull()
|
|
expect(
|
|
fitCircleArc([
|
|
{ x: 0, y: 0 },
|
|
{ x: 1, y: 0 }
|
|
])
|
|
).toBeNull()
|
|
})
|
|
|
|
test('fits points on a known circle', () => {
|
|
const r = 100
|
|
const pts = []
|
|
for (let i = 0; i <= 10; i++) {
|
|
const angle = (i / 10) * Math.PI
|
|
pts.push({ x: r * Math.cos(angle), y: r * Math.sin(angle) })
|
|
}
|
|
const result = fitCircleArc(pts)
|
|
const arc = expectDefined(result, 'fitted arc')
|
|
expect(arc.r).toBeCloseTo(r, 1)
|
|
expect(arc.cx).toBeCloseTo(0, 1)
|
|
expect(arc.cy).toBeCloseTo(0, 1)
|
|
})
|
|
|
|
test('returns null for collinear points', () => {
|
|
const pts = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 5, y: 0 },
|
|
{ x: 10, y: 0 }
|
|
]
|
|
expect(fitCircleArc(pts)).toBeNull()
|
|
})
|
|
|
|
test('returns null when points do not lie on a single circle', () => {
|
|
const pts = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 50, y: 100 },
|
|
{ x: 100, y: 0 },
|
|
{ x: 50, y: -200 }
|
|
]
|
|
expect(fitCircleArc(pts)).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('isClosedThinCrescent', () => {
|
|
test('detects annular wedge as crescent', () => {
|
|
const network = makeAnnularWedge(0, 0, 80, 100, 0, 180, 5)
|
|
const result = isClosedThinCrescent(network)
|
|
const crescent = expectDefined(result, 'crescent result')
|
|
expect(crescent.ordered.length).toBe(network.vertices.length)
|
|
})
|
|
|
|
test('rejects a simple rectangle (4 vertices)', () => {
|
|
const network: VectorNetwork = {
|
|
vertices: [
|
|
{ x: 0, y: 0 },
|
|
{ x: 100, y: 0 },
|
|
{ x: 100, y: 100 },
|
|
{ x: 0, y: 100 }
|
|
],
|
|
segments: [
|
|
{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 1, end: 2, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 2, end: 3, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 3, end: 0, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
|
|
],
|
|
regions: []
|
|
}
|
|
expect(isClosedThinCrescent(network)).toBeNull()
|
|
})
|
|
|
|
test('rejects a regular hexagon (thick shape)', () => {
|
|
const vertices = []
|
|
for (let i = 0; i < 6; i++) {
|
|
const angle = (i / 6) * 2 * Math.PI
|
|
vertices.push({ x: 100 * Math.cos(angle), y: 100 * Math.sin(angle) })
|
|
}
|
|
const segments = vertices.map((_, i) => ({
|
|
start: i,
|
|
end: (i + 1) % 6,
|
|
tangentStart: { x: 0, y: 0 },
|
|
tangentEnd: { x: 0, y: 0 }
|
|
}))
|
|
expect(isClosedThinCrescent({ vertices, segments, regions: [] })).toBeNull()
|
|
})
|
|
|
|
test('rejects open path (segments != vertices)', () => {
|
|
const network: VectorNetwork = {
|
|
vertices: [
|
|
{ x: 0, y: 0 },
|
|
{ x: 10, y: 0 },
|
|
{ x: 20, y: 0 }
|
|
],
|
|
segments: [
|
|
{ start: 0, end: 1, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } },
|
|
{ start: 1, end: 2, tangentStart: { x: 0, y: 0 }, tangentEnd: { x: 0, y: 0 } }
|
|
],
|
|
regions: []
|
|
}
|
|
expect(isClosedThinCrescent(network)).toBeNull()
|
|
})
|
|
|
|
test('rejects odd vertex count', () => {
|
|
const network = makeAnnularWedge(0, 0, 80, 100, 0, 180, 4)
|
|
const n = network.vertices.length
|
|
if (n % 2 === 0) {
|
|
network.vertices.push({ x: 999, y: 999 })
|
|
network.segments.push({
|
|
start: n - 1,
|
|
end: n,
|
|
tangentStart: { x: 0, y: 0 },
|
|
tangentEnd: { x: 0, y: 0 }
|
|
})
|
|
}
|
|
expect(isClosedThinCrescent(network)).toBeNull()
|
|
})
|
|
})
|