138 lines
5.0 KiB
TypeScript
138 lines
5.0 KiB
TypeScript
/**
|
||
* Task H5 – Flächenberechnung Polygon (m²) + measure-area Tool.
|
||
*
|
||
* polygonArea: Shoelace-Formel (Absolutwert, CW/CCW egal),
|
||
* formatArea: Einheiten-Formatierung analog formatDistance,
|
||
* measureAreaTool: Klick-Sequenz, Live-Fläche im Status, ENTER
|
||
* committet ab 3 Punkten, cancel setzt zurück.
|
||
*/
|
||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||
import { polygonArea } from '../src/tools/modification/geometry';
|
||
import { formatArea } from '../src/utils/format';
|
||
import { measureAreaTool } from '../src/plugins/builtin/core-measure';
|
||
import type { CADElement } from '../src/types/cad.types';
|
||
|
||
describe('H5: polygonArea (Shoelace)', () => {
|
||
it('Rechteck 100×50 = 5000 Welt-Einheiten²', () => {
|
||
const pts = [
|
||
{ x: 0, y: 0 }, { x: 100, y: 0 },
|
||
{ x: 100, y: 50 }, { x: 0, y: 50 },
|
||
];
|
||
expect(polygonArea(pts)).toBeCloseTo(5000, 6);
|
||
});
|
||
|
||
it('Dreieck 100×50/2 = 2500', () => {
|
||
const pts = [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 0, y: 50 }];
|
||
expect(polygonArea(pts)).toBeCloseTo(2500, 6);
|
||
});
|
||
|
||
it('Uhrzeigersinn und gegen Uhrzeigersinn ergeben denselben Absolutwert', () => {
|
||
const ccw = [{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 100, y: 100 }, { x: 0, y: 100 }];
|
||
const cw = [...ccw].reverse();
|
||
expect(polygonArea(cw)).toBeCloseTo(polygonArea(ccw), 6);
|
||
expect(polygonArea(cw)).toBeCloseTo(10000, 6);
|
||
});
|
||
|
||
it('weniger als 3 Punkte → 0; kollineare Punkte → 0', () => {
|
||
expect(polygonArea([{ x: 0, y: 0 }, { x: 1, y: 1 }])).toBe(0);
|
||
expect(polygonArea([{ x: 0, y: 0 }, { x: 100, y: 0 }, { x: 200, y: 0 }])).toBe(0);
|
||
});
|
||
});
|
||
|
||
describe('H5: formatArea', () => {
|
||
it('mm: 5000 → "5000 mm²" (scaleFactor 1)', () => {
|
||
expect(formatArea(5000, 'mm', 1)).toBe('5000 mm²');
|
||
});
|
||
|
||
it('cm: 5000mm² → "50.0 cm²"', () => {
|
||
expect(formatArea(5000, 'cm', 1)).toBe('50.0 cm²');
|
||
});
|
||
|
||
it('m: 5000000mm² → "5.00 m²"', () => {
|
||
expect(formatArea(5000000, 'm', 1)).toBe('5.00 m²');
|
||
});
|
||
|
||
it('Flächen skalieren QUADRATISCH: 5 WE² bei scaleFactor 1000 = 5000000 mm²', () => {
|
||
// 1 Welt-Einheit = 1000mm → 1 WE² = 1.000.000 mm²
|
||
expect(formatArea(5, 'mm', 1000)).toBe('5000000 mm²');
|
||
});
|
||
});
|
||
|
||
describe('H5: measureAreaTool', () => {
|
||
// Module-State areaPts leakt zwischen Tests — vor jedem Test resetten:
|
||
beforeEach(() => {
|
||
measureAreaTool.handlers.cancel?.({ setStatus: () => {} } as never);
|
||
});
|
||
function fireSeq(events: Array<{ type: 'down' | 'move' | 'key' | 'cancel'; x?: number; y?: number; key?: string }>, options: Record<string, unknown> = {}) {
|
||
const statuses: string[] = [];
|
||
const ctx: any = {
|
||
options,
|
||
setStatus: (m: string) => statuses.push(m),
|
||
setPreview: (_el: CADElement | null) => {},
|
||
doc: null,
|
||
};
|
||
for (const ev of events) {
|
||
if (ev.type === 'down') measureAreaTool.handlers.down!({ world: { x: ev.x!, y: ev.y! } } as never, ctx);
|
||
if (ev.type === 'move') measureAreaTool.handlers.move!({ world: { x: ev.x!, y: ev.y! } } as never, ctx);
|
||
if (ev.type === 'key' && ev.key) measureAreaTool.handlers.key?.({ key: ev.key } as never, ctx);
|
||
if (ev.type === 'cancel') measureAreaTool.handlers.cancel?.(ctx);
|
||
}
|
||
return statuses;
|
||
}
|
||
|
||
it('erster Klick startet Messung, Status fordert weitere Punkte', () => {
|
||
const s = fireSeq([{ type: 'down', x: 0, y: 0 }]);
|
||
expect(s[0]).toContain('Fläche');
|
||
expect(s[s.length - 1]).toContain('klicken');
|
||
});
|
||
|
||
it('Live-Fläche im Status ab 3 Punkten bei move', () => {
|
||
const s = fireSeq([
|
||
{ type: 'down', x: 0, y: 0 },
|
||
{ type: 'down', x: 100, y: 0 },
|
||
{ type: 'down', x: 100, y: 50 },
|
||
{ type: 'move', x: 0, y: 50 },
|
||
]);
|
||
expect(s.some((m) => m.includes('5000'))).toBe(true);
|
||
expect(s[s.length - 1]).toContain('mm²');
|
||
});
|
||
|
||
it('ENTER committet ab 3 Punkten mit Endfläche im Status', () => {
|
||
const s = fireSeq([
|
||
{ type: 'down', x: 0, y: 0 },
|
||
{ type: 'down', x: 100, y: 0 },
|
||
{ type: 'down', x: 100, y: 100 },
|
||
{ type: 'key', key: 'Enter' },
|
||
]);
|
||
// Dreieck (0,0),(100,0),(100,100): Shoelace = 1/2 · 100 · 100 = 5000
|
||
expect(s[s.length - 1]).toContain('5000');
|
||
expect(s[s.length - 1]).toContain('3 Punkte');
|
||
});
|
||
|
||
it('ENTER mit weniger als 3 Punkten committet NICHT (Status erklärt)', () => {
|
||
const s = fireSeq([
|
||
{ type: 'down', x: 0, y: 0 },
|
||
{ type: 'down', x: 100, y: 0 },
|
||
{ type: 'key', key: 'Enter' },
|
||
]);
|
||
expect(s[s.length - 1]).toContain('3');
|
||
});
|
||
|
||
it('cancel setzt Messung zurück', () => {
|
||
const s = fireSeq([
|
||
{ type: 'down', x: 0, y: 0 },
|
||
{ type: 'down', x: 50, y: 50 },
|
||
{ type: 'cancel' },
|
||
{ type: 'down', x: 0, y: 0 },
|
||
]);
|
||
// Nach cancel startet eine frische Messung (kein altes Polygon)
|
||
expect(s[s.length - 1]).toContain('klicken');
|
||
});
|
||
|
||
it('Manifest: id measure-area, tags basic, ribbonTab canvas', () => {
|
||
expect(measureAreaTool.manifest.id).toBe('measure-area');
|
||
expect(measureAreaTool.manifest.ribbonTab).toBe('canvas');
|
||
expect((measureAreaTool.manifest.tags ?? [])).toContain('basic');
|
||
});
|
||
});
|