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