openpencil/tests/engine/vector/centerline.test.ts
2026-06-30 10:54:32 +03:00

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()
})
})