/** * 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 = {}) { 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'); }); });