/** * B07 CAD 기하 순수 함수 검증 (조사표 1·2절 명령의 계산 부분). * 실행: cd B07_DesignDetail/openwebcad && npx vitest run --root ../.. tmp/tests/cad */ import { Point } from '@flatten-js/core'; import { describe, expect, it } from 'vitest'; import { hatchSpans, isPointInPolygon } from '../../../B07_DesignDetail/openwebcad/src/helpers/geometry/hatch-lines'; import { dividePoints, measurePoints, pointAtDistance, polylineLength, } from '../../../B07_DesignDetail/openwebcad/src/helpers/geometry/sample-entity'; import { arcThroughThreePoints, ellipsePoints, intersectLines, offsetPolylinePoints, regularPolygonPoints, revisionCloudPoints, splinePoints, } from '../../../B07_DesignDetail/openwebcad/src/helpers/geometry/shape-points'; const p = (x: number, y: number) => new Point(x, y); describe('3점 호 (ARC)', () => { it('단위원 위의 세 점이면 중심은 원점, 반지름은 1이다', () => { const arc = arcThroughThreePoints(p(1, 0), p(0, 1), p(-1, 0)); expect(arc).not.toBeNull(); expect(arc?.center.x).toBeCloseTo(0, 6); expect(arc?.center.y).toBeCloseTo(0, 6); expect(arc?.radius).toBeCloseTo(1, 6); }); it('일직선이면 호가 성립하지 않는다', () => { expect(arcThroughThreePoints(p(0, 0), p(1, 0), p(2, 0))).toBeNull(); }); }); describe('정다각형 (POLYGON)', () => { it('변 수만큼 꼭짓점을 만들고 마지막에 닫는다', () => { const points = regularPolygonPoints(p(0, 0), p(1, 0), 6); expect(points).toHaveLength(7); // 꼭짓점 6 + 닫는 점 expect(points[6].x).toBeCloseTo(points[0].x, 9); for (const point of points) { expect(Math.hypot(point.x, point.y)).toBeCloseTo(1, 9); } }); }); describe('타원 (ELLIPSE)', () => { it('장축·단축 반지름이 지정한 값과 같다', () => { const points = ellipsePoints(p(0, 0), p(2, 0), 1, 4); expect(points[0].x).toBeCloseTo(2, 9); // 장축 방향 expect(points[1].y).toBeCloseTo(1, 9); // 90°에서 단축 반지름 }); }); describe('간격띄우기 (OFFSET·MLINE)', () => { it('수평선을 왼쪽으로 밀면 y가 커진다', () => { const offset = offsetPolylinePoints([p(0, 0), p(10, 0)], 2); expect(offset[0].y).toBeCloseTo(2, 9); expect(offset[offset.length - 1].y).toBeCloseTo(2, 9); }); it('꺾인 폴리선은 이음매에서 두 평행선의 교점을 쓴다', () => { const offset = offsetPolylinePoints([p(0, 0), p(10, 0), p(10, 10)], 1); expect(offset).toHaveLength(3); expect(offset[1].x).toBeCloseTo(9, 9); expect(offset[1].y).toBeCloseTo(1, 9); }); }); describe('직선 교점 (FILLET·CHAMFER·EXTEND)', () => { it('직교하는 두 직선의 교점을 찾는다', () => { const crossing = intersectLines(p(0, 0), p(10, 0), p(4, -5), p(4, 5)); expect(crossing?.x).toBeCloseTo(4, 9); expect(crossing?.y).toBeCloseTo(0, 9); }); it('평행하면 교점이 없다', () => { expect(intersectLines(p(0, 0), p(10, 0), p(0, 3), p(10, 3))).toBeNull(); }); }); describe('점렬 길이와 분할 (DIVIDE·MEASURE)', () => { const line = [p(0, 0), p(10, 0)]; it('길이를 누적해서 잰다', () => { expect(polylineLength([p(0, 0), p(3, 4), p(3, 4)])).toBeCloseTo(5, 9); }); it('지정 거리 위치의 점을 찾는다', () => { expect(pointAtDistance(line, 2.5)?.x).toBeCloseTo(2.5, 9); expect(pointAtDistance(line, 20)).toBeNull(); }); it('등분은 내부 점만 만든다', () => { const points = dividePoints(line, 4); expect(points).toHaveLength(3); expect(points.map((point) => point.x)).toEqual([2.5, 5, 7.5]); }); it('길이분할은 지정 간격마다 점을 둔다', () => { expect(measurePoints(line, 4).map((point) => point.x)).toEqual([4, 8]); }); }); describe('해치 스캔선 (HATCH)', () => { const square = [p(0, 0), p(10, 0), p(10, 10), p(0, 10)]; it('사각형 내부를 가로지르는 선분을 만든다', () => { const spans = hatchSpans(square, 0, 2); expect(spans.length).toBeGreaterThan(3); for (const [start, end] of spans) { expect(start.x).toBeCloseTo(0, 6); expect(end.x).toBeCloseTo(10, 6); expect(start.y).toBeGreaterThan(0); expect(start.y).toBeLessThan(10); } }); it('점이 다각형 안에 있는지 가른다', () => { expect(isPointInPolygon(square, p(5, 5))).toBe(true); expect(isPointInPolygon(square, p(15, 5))).toBe(false); }); }); describe('스플라인·구름형 (SPLINE·REVCLOUD)', () => { it('스플라인은 시작·끝 조정점을 지난다', () => { const curve = splinePoints([p(0, 0), p(5, 5), p(10, 0)], 8); expect(curve[0].x).toBeCloseTo(0, 9); expect(curve[curve.length - 1].x).toBeCloseTo(10, 9); expect(curve.length).toBeGreaterThan(10); }); it('구름형은 경로 길이에 맞춰 스캘럽을 채운다', () => { const cloud = revisionCloudPoints([p(0, 0), p(10, 0)], 1); expect(cloud.length).toBeGreaterThan(20); expect(cloud[0].x).toBeCloseTo(0, 6); }); });