/** * geometry.ts Tests – Pure transformation functions (move, rotate, scale, mirror) */ import { describe, it, expect } from 'vitest'; import { moveElement, rotateElement, scaleElement, mirrorElement, offsetElement, getElementBBox, distance, angleBetween, } from '../src/tools/modification/geometry'; import type { CADElement } from '../src/types/cad.types'; function makeLine(id: string, x1: number, y1: number, x2: number, y2: number): CADElement { return { id, type: 'line', layerId: 'layer-1', x: (x1 + x2) / 2, y: (y1 + y2) / 2, width: Math.abs(x2 - x1), height: Math.abs(y2 - y1), properties: { x1, y1, x2, y2 }, }; } function makeRect(id: string, cx: number, cy: number, w: number, h: number): CADElement { return { id, type: 'rect', layerId: 'layer-1', x: cx, y: cy, width: w, height: h, properties: {}, }; } function makeCircle(id: string, cx: number, cy: number, r: number): CADElement { return { id, type: 'circle', layerId: 'layer-1', x: cx, y: cy, width: r * 2, height: r * 2, properties: { radius: r }, }; } function makePolyline(id: string, points: Array<{x:number;y:number}>): CADElement { const xs = points.map(p => p.x); const ys = points.map(p => p.y); return { id, type: 'polyline', layerId: 'layer-1', x: (Math.min(...xs) + Math.max(...xs)) / 2, y: (Math.min(...ys) + Math.max(...ys)) / 2, width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys), properties: { points }, }; } describe('geometry – moveElement', () => { it('should shift x and y by dx, dy', () => { const el = makeRect('r1', 100, 100, 40, 40); const moved = moveElement(el, 50, 30); expect(moved.x).toBe(150); expect(moved.y).toBe(130); }); it('should shift line endpoint properties', () => { const el = makeLine('l1', 0, 0, 100, 0); const moved = moveElement(el, 50, 30); expect(moved.properties.x1).toBe(50); expect(moved.properties.y1).toBe(30); expect(moved.properties.x2).toBe(150); expect(moved.properties.y2).toBe(30); }); it('should shift polyline points', () => { const el = makePolyline('p1', [{ x: 0, y: 0 }, { x: 100, y: 50 }]); const moved = moveElement(el, 10, 20); expect(moved.properties.points![0].x).toBe(10); expect(moved.properties.points![0].y).toBe(20); expect(moved.properties.points![1].x).toBe(110); expect(moved.properties.points![1].y).toBe(70); }); it('should not modify the original element (immutability)', () => { const el = makeLine('l1', 0, 0, 100, 0); const original = JSON.parse(JSON.stringify(el)); moveElement(el, 50, 50); expect(el).toEqual(original); }); }); describe('geometry – rotateElement', () => { it('should rotate element position around center', () => { const el = makeRect('r1', 100, 0, 40, 40); const rotated = rotateElement(el, 0, 0, 90); expect(rotated.x).toBeCloseTo(0, 0); expect(rotated.y).toBeCloseTo(100, 0); }); it('should rotate line endpoints around center', () => { const el = makeLine('l1', 100, 0, 200, 0); const rotated = rotateElement(el, 0, 0, 90); expect(rotated.properties.x1).toBeCloseTo(0, 0); expect(rotated.properties.y1).toBeCloseTo(100, 0); expect(rotated.properties.x2).toBeCloseTo(0, 0); expect(rotated.properties.y2).toBeCloseTo(200, 0); }); it('should rotate 180 degrees correctly', () => { const el = makeRect('r1', 100, 0, 40, 40); const rotated = rotateElement(el, 0, 0, 180); expect(rotated.x).toBeCloseTo(-100, 0); expect(rotated.y).toBeCloseTo(0, 0); }); it('should rotate 360 degrees back to original position', () => { const el = makeRect('r1', 100, 0, 40, 40); const rotated = rotateElement(el, 0, 0, 360); expect(rotated.x).toBeCloseTo(100, 5); expect(rotated.y).toBeCloseTo(0, 5); }); it('should swap width/height for 90-degree rotation on rect', () => { const el = makeRect('r1', 100, 100, 80, 40); const rotated = rotateElement(el, 100, 100, 90); expect(rotated.width).toBe(40); expect(rotated.height).toBe(80); }); it('should add rotation to properties.rotation', () => { const el = makeRect('r1', 100, 100, 40, 40); el.properties.rotation = 30; const rotated = rotateElement(el, 100, 100, 45); expect(rotated.properties.rotation).toBe(75); }); it('should not modify the original element', () => { const el = makeLine('l1', 100, 0, 200, 0); const original = JSON.parse(JSON.stringify(el)); rotateElement(el, 0, 0, 90); expect(el).toEqual(original); }); }); describe('geometry – scaleElement', () => { it('should scale element position and dimensions', () => { const el = makeRect('r1', 100, 100, 40, 40); const scaled = scaleElement(el, 100, 100, 2, 2); expect(scaled.x).toBe(100); expect(scaled.y).toBe(100); expect(scaled.width).toBe(80); expect(scaled.height).toBe(80); }); it('should scale element away from center', () => { const el = makeRect('r1', 100, 0, 40, 40); const scaled = scaleElement(el, 0, 0, 2, 2); expect(scaled.x).toBe(200); expect(scaled.y).toBe(0); expect(scaled.width).toBe(80); expect(scaled.height).toBe(80); }); it('should scale line endpoints', () => { const el = makeLine('l1', 100, 0, 200, 0); const scaled = scaleElement(el, 0, 0, 2, 2); expect(scaled.properties.x1).toBe(200); expect(scaled.properties.x2).toBe(400); }); it('should scale circle radius', () => { const el = makeCircle('c1', 100, 100, 40); const scaled = scaleElement(el, 100, 100, 2, 2); expect(scaled.properties.radius).toBe(80); }); it('should not modify the original element', () => { const el = makeRect('r1', 100, 100, 40, 40); const original = JSON.parse(JSON.stringify(el)); scaleElement(el, 100, 100, 2, 2); expect(el).toEqual(original); }); }); describe('geometry – mirrorElement', () => { it('should mirror across a vertical line', () => { const el = makeRect('r1', 100, 0, 40, 40); const mirrored = mirrorElement(el, 200, 0, 200, 100); expect(mirrored.x).toBe(300); expect(mirrored.y).toBe(0); }); it('should mirror across a horizontal line', () => { const el = makeRect('r1', 0, 100, 40, 40); const mirrored = mirrorElement(el, 0, 200, 100, 200); expect(mirrored.x).toBe(0); expect(mirrored.y).toBe(300); }); it('should mirror line endpoints', () => { const el = makeLine('l1', 0, 0, 100, 0); const mirrored = mirrorElement(el, 50, 0, 50, 100); expect(mirrored.properties.x1).toBe(100); expect(mirrored.properties.x2).toBe(0); }); it('should return element unchanged for zero-length mirror axis', () => { const el = makeRect('r1', 100, 100, 40, 40); const mirrored = mirrorElement(el, 50, 50, 50, 50); expect(mirrored.x).toBe(100); expect(mirrored.y).toBe(100); }); it('should not modify the original element', () => { const el = makeLine('l1', 0, 0, 100, 0); const original = JSON.parse(JSON.stringify(el)); mirrorElement(el, 50, 0, 50, 100); expect(el).toEqual(original); }); }); describe('geometry – offsetElement', () => { it('should offset a line perpendicularly', () => { const el = makeLine('l1', 0, 0, 100, 0); const offset = offsetElement(el, 10); expect(offset.properties.y1).toBe(10); expect(offset.properties.y2).toBe(10); }); it('should offset a circle radius', () => { const el = makeCircle('c1', 100, 100, 40); const offset = offsetElement(el, 10); expect(offset.properties.radius).toBe(50); }); it('should return element for unknown type', () => { const el = makeRect('r1', 100, 100, 40, 40); const offset = offsetElement(el, 10); expect(offset.x).toBe(100); expect(offset.y).toBe(100); }); }); describe('geometry – getElementBBox', () => { it('should return bbox for rect element', () => { const el = makeRect('r1', 100, 100, 40, 60); const bb = getElementBBox(el); expect(bb.minX).toBe(80); expect(bb.minY).toBe(70); expect(bb.maxX).toBe(120); expect(bb.maxY).toBe(130); }); it('should return bbox for polyline element from points', () => { const el = makePolyline('p1', [{ x: 10, y: 20 }, { x: 100, y: 80 }]); const bb = getElementBBox(el); expect(bb.minX).toBe(10); expect(bb.minY).toBe(20); expect(bb.maxX).toBe(100); expect(bb.maxY).toBe(80); }); }); describe('geometry – distance', () => { it('should calculate distance between two points', () => { expect(distance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBe(5); }); it('should return 0 for same point', () => { expect(distance({ x: 5, y: 5 }, { x: 5, y: 5 })).toBe(0); }); }); describe('geometry – angleBetween', () => { it('should calculate angle between two points in degrees', () => { expect(angleBetween({ x: 0, y: 0 }, { x: 100, y: 0 })).toBe(0); }); it('should calculate 90-degree angle', () => { expect(angleBetween({ x: 0, y: 0 }, { x: 0, y: 100 })).toBe(90); }); });