mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
244 lines
8.1 KiB
TypeScript
244 lines
8.1 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { BezierPath, BezierSegment, Point2D, Point3D } from '../src/types'
|
|
import { arcLengthTo, pointAt, segmentLength, segmentStartPoints, splitSegmentAt, tangentAt } from '../src/segment'
|
|
import { expectPointClose } from './helpers'
|
|
|
|
const start: Point2D = { x: 0, y: 0 }
|
|
const seg: BezierSegment<Point2D> = { cp1: { x: 0, y: 100 }, cp2: { x: 100, y: 100 }, end: { x: 100, y: 0 } }
|
|
|
|
describe('pointAt (2D)', () => {
|
|
it('t=0 で始点を返す', () => {
|
|
expectPointClose(pointAt(start, seg, 0), start)
|
|
})
|
|
|
|
it('t=1 で終点を返す', () => {
|
|
expectPointClose(pointAt(start, seg, 1), seg.end)
|
|
})
|
|
|
|
it('t=0.5 で中間付近の座標を返す', () => {
|
|
const mid = pointAt(start, seg, 0.5)
|
|
expect(mid.x).toBeGreaterThan(0)
|
|
expect(mid.x).toBeLessThan(100)
|
|
expect(mid.y).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('t < 0 / t > 1 でも外挿される', () => {
|
|
const before = pointAt(start, seg, -0.5)
|
|
const after = pointAt(start, seg, 1.5)
|
|
expect(Number.isFinite(before.x)).toBe(true)
|
|
expect(Number.isFinite(after.x)).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('pointAt (3D)', () => {
|
|
const start3d: Point3D = { x: 0, y: 0, z: 0 }
|
|
const seg3d: BezierSegment<Point3D> = {
|
|
cp1: { x: 0, y: 100, z: 50 },
|
|
cp2: { x: 100, y: 100, z: 50 },
|
|
end: { x: 100, y: 0, z: 100 },
|
|
}
|
|
|
|
it('t=0 で始点を返し z も含む', () => {
|
|
const p = pointAt(start3d, seg3d, 0)
|
|
expect(p).toEqual(start3d)
|
|
expect('z' in p).toBe(true)
|
|
})
|
|
|
|
it('t=1 で終点を返す(z=100)', () => {
|
|
const p = pointAt(start3d, seg3d, 1)
|
|
expect(p.z).toBe(100)
|
|
})
|
|
|
|
it('t=0.5 で z が中間値域', () => {
|
|
const p = pointAt(start3d, seg3d, 0.5)
|
|
expect(p.z).toBeGreaterThan(0)
|
|
expect(p.z).toBeLessThan(100)
|
|
})
|
|
})
|
|
|
|
describe('tangentAt (2D)', () => {
|
|
it('t=0 の接線は 3 * (cp1 - start)', () => {
|
|
const v = tangentAt(start, seg, 0)
|
|
expectPointClose(v, { x: 3 * (seg.cp1.x - start.x), y: 3 * (seg.cp1.y - start.y) })
|
|
})
|
|
|
|
it('t=1 の接線は 3 * (end - cp2)', () => {
|
|
const v = tangentAt(start, seg, 1)
|
|
expectPointClose(v, { x: 3 * (seg.end.x - seg.cp2.x), y: 3 * (seg.end.y - seg.cp2.y) })
|
|
})
|
|
|
|
it('対称な山型の曲線では t=0.5 の接線は水平(x 方向)', () => {
|
|
const tan = tangentAt(start, seg, 0.5)
|
|
expect(Math.abs(tan.y)).toBeLessThan(1e-9)
|
|
expect(tan.x).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('数値微分とほぼ一致する(差分法)', () => {
|
|
const t = 0.3
|
|
const h = 1e-5
|
|
const p0 = pointAt(start, seg, t - h)
|
|
const p1 = pointAt(start, seg, t + h)
|
|
const numeric = { x: (p1.x - p0.x) / (2 * h), y: (p1.y - p0.y) / (2 * h) }
|
|
const analytic = tangentAt(start, seg, t)
|
|
expectPointClose(analytic, numeric, 3)
|
|
})
|
|
})
|
|
|
|
describe('tangentAt (3D)', () => {
|
|
const start3d: Point3D = { x: 0, y: 0, z: 0 }
|
|
const seg3d: BezierSegment<Point3D> = {
|
|
cp1: { x: 10, y: 0, z: 0 },
|
|
cp2: { x: 20, y: 0, z: 0 },
|
|
end: { x: 30, y: 0, z: 0 },
|
|
}
|
|
|
|
it('t=0 の接線は 3 * (cp1 - start)、z 成分も 3D', () => {
|
|
const v = tangentAt(start3d, seg3d, 0)
|
|
expect(v.x).toBeCloseTo(30, 6)
|
|
expect(v.y).toBeCloseTo(0, 6)
|
|
expect(v.z).toBeCloseTo(0, 6)
|
|
})
|
|
|
|
it('z 軸沿いのセグメントでは接線の z が 0 ではない', () => {
|
|
const zSeg: BezierSegment<Point3D> = {
|
|
cp1: { x: 0, y: 0, z: 10 },
|
|
cp2: { x: 0, y: 0, z: 20 },
|
|
end: { x: 0, y: 0, z: 30 },
|
|
}
|
|
const v = tangentAt(start3d, zSeg, 0.5)
|
|
expect(v.x).toBeCloseTo(0, 6)
|
|
expect(v.y).toBeCloseTo(0, 6)
|
|
expect(v.z).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('splitSegmentAt (2D)', () => {
|
|
it('分割点が元の曲線上の点と一致する', () => {
|
|
const t = 0.4
|
|
const [left] = splitSegmentAt(start, seg, t)
|
|
expectPointClose(left.end, pointAt(start, seg, t))
|
|
})
|
|
|
|
it('前半の終点と後半の始点(= 前半の end)が曲線上の点に一致', () => {
|
|
const [left] = splitSegmentAt(start, seg, 0.5)
|
|
expectPointClose(left.end, pointAt(start, seg, 0.5))
|
|
})
|
|
|
|
it('後半の終点が元のセグメントの終点と一致する', () => {
|
|
const [, right] = splitSegmentAt(start, seg, 0.3)
|
|
expectPointClose(right.end, seg.end)
|
|
})
|
|
|
|
it('t=0 で左は縮退、右は元のセグメントに近い', () => {
|
|
const [left, right] = splitSegmentAt(start, seg, 0)
|
|
expectPointClose(left.end, start)
|
|
expectPointClose(right.end, seg.end)
|
|
})
|
|
})
|
|
|
|
describe('splitSegmentAt (3D)', () => {
|
|
const start3d: Point3D = { x: 0, y: 0, z: 0 }
|
|
const seg3d: BezierSegment<Point3D> = {
|
|
cp1: { x: 10, y: 100, z: 50 },
|
|
cp2: { x: 90, y: 100, z: 50 },
|
|
end: { x: 100, y: 0, z: 100 },
|
|
}
|
|
|
|
it('分割点が pointAt と一致(z 含む)', () => {
|
|
const [left] = splitSegmentAt(start3d, seg3d, 0.4)
|
|
const p = pointAt(start3d, seg3d, 0.4)
|
|
expect(left.end.x).toBeCloseTo(p.x, 6)
|
|
expect(left.end.y).toBeCloseTo(p.y, 6)
|
|
expect(left.end.z).toBeCloseTo(p.z, 6)
|
|
})
|
|
|
|
it('後半の終点が元のセグメントの終点と一致する(z 含む)', () => {
|
|
const [, right] = splitSegmentAt(start3d, seg3d, 0.3)
|
|
expect(right.end).toEqual(seg3d.end)
|
|
})
|
|
})
|
|
|
|
describe('arcLengthTo / segmentLength', () => {
|
|
const lineStart: Point2D = { x: 0, y: 0 }
|
|
const lineSeg: BezierSegment<Point2D> = {
|
|
cp1: { x: 33.33, y: 0 },
|
|
cp2: { x: 66.67, y: 0 },
|
|
end: { x: 100, y: 0 },
|
|
}
|
|
|
|
it('直線セグメントの全長は始点-終点間の距離に近い', () => {
|
|
const len = segmentLength(lineStart, lineSeg)
|
|
expect(len).toBeCloseTo(100, 0)
|
|
})
|
|
|
|
it('tEnd=0 の弧長は 0', () => {
|
|
expect(arcLengthTo(lineStart, lineSeg, 0)).toBe(0)
|
|
})
|
|
|
|
it('tEnd=0.5 の弧長は全長の約半分', () => {
|
|
const half = arcLengthTo(lineStart, lineSeg, 0.5)
|
|
const full = segmentLength(lineStart, lineSeg)
|
|
expect(half).toBeCloseTo(full / 2, 0)
|
|
})
|
|
|
|
it('samples を増やすと精度が上がる(直線では同じだが発散しない)', () => {
|
|
const lowSamples = segmentLength(lineStart, lineSeg, { samples: 4 })
|
|
const highSamples = segmentLength(lineStart, lineSeg, { samples: 256 })
|
|
expect(Math.abs(highSamples - lowSamples)).toBeLessThan(1)
|
|
})
|
|
|
|
it('退化セグメント(全ての点が同じ)の長さは 0', () => {
|
|
const degenerate: BezierSegment<Point2D> = { cp1: start, cp2: start, end: start }
|
|
expect(segmentLength(start, degenerate)).toBeCloseTo(0, 10)
|
|
})
|
|
|
|
it('3D 直線セグメントの長さは 3D 距離に近い', () => {
|
|
const s: Point3D = { x: 0, y: 0, z: 0 }
|
|
const lineSeg3d: BezierSegment<Point3D> = {
|
|
cp1: { x: 1, y: 2, z: 2 },
|
|
cp2: { x: 2, y: 4, z: 4 },
|
|
end: { x: 3, y: 6, z: 6 },
|
|
}
|
|
// (0,0,0) → (3,6,6) の距離 = 9
|
|
const len = segmentLength(s, lineSeg3d)
|
|
expect(len).toBeCloseTo(9, 0)
|
|
})
|
|
})
|
|
|
|
describe('segmentStartPoints', () => {
|
|
it('各セグメントの始点を正しく返す', () => {
|
|
const path: BezierPath<Point2D> = {
|
|
start: { x: 0, y: 0 },
|
|
segments: [
|
|
{ cp1: { x: 1, y: 1 }, cp2: { x: 2, y: 2 }, end: { x: 10, y: 0 } },
|
|
{ cp1: { x: 11, y: 1 }, cp2: { x: 12, y: 2 }, end: { x: 20, y: 0 } },
|
|
{ cp1: { x: 21, y: 1 }, cp2: { x: 22, y: 2 }, end: { x: 30, y: 0 } },
|
|
],
|
|
}
|
|
const points = segmentStartPoints(path)
|
|
expect(points).toHaveLength(3)
|
|
expectPointClose(points[0]!, { x: 0, y: 0 })
|
|
expectPointClose(points[1]!, { x: 10, y: 0 })
|
|
expectPointClose(points[2]!, { x: 20, y: 0 })
|
|
})
|
|
|
|
it('セグメントなしのパスでは [start] のみ(空を返さないよう)', () => {
|
|
const path: BezierPath<Point2D> = { start: { x: 7, y: 3 }, segments: [] }
|
|
const points = segmentStartPoints(path)
|
|
expect(points).toEqual([{ x: 7, y: 3 }])
|
|
})
|
|
|
|
it('3D パスの startPoints は z を持つ', () => {
|
|
const path: BezierPath<Point3D> = {
|
|
start: { x: 0, y: 0, z: 0 },
|
|
segments: [
|
|
{ cp1: { x: 0, y: 0, z: 0 }, cp2: { x: 0, y: 0, z: 5 }, end: { x: 10, y: 0, z: 10 } },
|
|
{ cp1: { x: 10, y: 5, z: 10 }, cp2: { x: 20, y: 5, z: 15 }, end: { x: 30, y: 0, z: 20 } },
|
|
],
|
|
}
|
|
const points = segmentStartPoints(path)
|
|
expect(points).toHaveLength(2)
|
|
expect(points[0]!.z).toBe(0)
|
|
expect(points[1]!.z).toBe(10)
|
|
})
|
|
})
|