Files
web-cad/frontend/tests/geometry.test.ts
T
Leopoldadmin 1bcfaffdd4 fix: replace hardcoded localhost:3001 with relative API URL + add canvas mock and integration test
- Fix frontend Network error: API_BASE now defaults to empty string (same-origin)
- Fix 3 files: api.ts, AuthContext.tsx, Dashboard.tsx
- Add Canvas 2D context mock in tests/setup.ts for jsdom
- Fix -0 vs +0 in ZoomPanController.getViewport()
- Add IntegrationWorkflow.test.ts (35 tests, full CAD workflow)
2026-06-26 17:48:24 +02:00

296 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
});
});