/** * Task CAD-1 - Assoziative Bemaessung: DimStyles + 5 Typen. * * DimStyle: textHeight/arrowSize/unit/decimals/arrowStyle/offset. * computeDimValue: berechnet Masswert aus Geometrie (assoziativ). * Tools: dimLinear (H/V/aligned), dimAngular, dimRadius, dimDiameter. * RenderAdapter: komplette Primitives mit Extension lines, Masslinien, * Pfeilen und Text. */ import { describe, it, expect, beforeEach } from 'vitest'; import { DEFAULT_DIM_STYLE, DIM_STYLES, getDimStyle, computeDimValue, formatDimValue, type DimStyle, } from '../src/services/dimStyleService'; import { dimLinearTool, dimAlignedTool, dimAngularTool, dimRadiusTool, dimDiameterTool, } from '../src/plugins/builtin/core-annotate'; import { CADDocument } from '../src/kernel/document/CADDocument'; import { createInMemoryDoc } from './helpers/inMemoryDoc'; import { elementToPrimitives } from '../src/render/pixi/elementDrawers'; import type { CADElement, Pt } from '../src/types/cad.types'; function mkCtx(options: Record = {}) { const { ydoc } = createInMemoryDoc(); const doc = new CADDocument(ydoc); const statuses: string[] = []; const previews: (CADElement | null)[] = []; const ctx: any = { doc, options, setStatus: (m: string) => statuses.push(m), setPreview: (el: CADElement | null) => previews.push(el), }; return { ctx, doc, statuses, previews }; } function pe(x: number, y: number): { world: Pt } { return { world: { x, y } } as never; } beforeEach(() => { for (const t of [dimLinearTool, dimAlignedTool, dimAngularTool, dimRadiusTool, dimDiameterTool]) { t.handlers.cancel?.({ setStatus: () => {}, setPreview: () => {} } as never); } }); // ─── DimStyle-System ───────────────────────────────────────── describe('CAD-1: DimStyles', () => { it('DEFAULT_DIM_STYLE ist ISO-konform', () => { expect(DEFAULT_DIM_STYLE.name).toBe('ISO-25'); expect(DEFAULT_DIM_STYLE.textHeight).toBe(2.5); expect(DEFAULT_DIM_STYLE.arrowSize).toBe(2.5); expect(DEFAULT_DIM_STYLE.unit).toBe('mm'); expect(DEFAULT_DIM_STYLE.decimals).toBe(0); expect(DEFAULT_DIM_STYLE.arrowStyle).toBe('closed'); }); it('DIM_STYLES enthaelt Standard-Set (ISO-25, ISO-50, Architectural)', () => { expect(DIM_STYLES.map((s) => s.name)).toContain('ISO-25'); expect(DIM_STYLES.map((s) => s.name)).toContain('ISO-50'); expect(DIM_STYLES.map((s) => s.name)).toContain('Architectural'); }); it('getDimStyle liefert Default fuer unbekannten Namen', () => { expect(getDimStyle('gibts-nicht')).toBe(DEFAULT_DIM_STYLE); expect(getDimStyle('ISO-50')!.textHeight).toBe(5); }); }); // ─── computeDimValue: Assoziative Wert-Berechnung ─────────── describe('CAD-1: computeDimValue', () => { it('linear horizontal: |x2-x1|', () => { expect(computeDimValue('linear', { x: 0, y: 0 }, { x: 100, y: 50 })).toBe(100); }); it('linear vertikal: |y2-y1|', () => { expect(computeDimValue('linear', { x: 0, y: 0 }, { x: 50, y: 200 })).toBe(200); }); it('aligned: Distanz zwischen Punkten', () => { expect(computeDimValue('aligned', { x: 0, y: 0 }, { x: 30, y: 40 })).toBe(50); }); it('radius: r aus Kreis-Element', () => { const circle = { x: 0, y: 0, properties: { radius: 25 } } as unknown as CADElement; expect(computeDimValue('radius', undefined, undefined, circle)).toBe(25); }); it('diameter: 2*r aus Kreis-Element', () => { const circle = { x: 0, y: 0, properties: { radius: 25 } } as unknown as CADElement; expect(computeDimValue('diameter', undefined, undefined, circle)).toBe(50); }); }); // ─── formatDimValue ────────────────────────────────────────── describe('CAD-1: formatDimValue', () => { it('mm ohne Dezimalstellen bei decimals=0', () => { expect(formatDimValue(123.7, DEFAULT_DIM_STYLE)).toBe('124'); }); it('mm mit 2 Dezimalstellen', () => { const s = { ...DEFAULT_DIM_STYLE, decimals: 2 }; expect(formatDimValue(123.456, s)).toBe('123.46'); }); it('Radius-Präfix bei radius-Typ', () => { expect(formatDimValue(25, DEFAULT_DIM_STYLE, 'radius')).toBe('R25'); }); it('Durchmesser-Symbol bei diameter-Typ', () => { expect(formatDimValue(50, DEFAULT_DIM_STYLE, 'diameter')).toBe('\u2300 50'); }); it('Grad bei angular', () => { expect(formatDimValue(45, DEFAULT_DIM_STYLE, 'angular')).toBe('45\u00b0'); }); }); // ─── dimLinearTool ───────────────────────────────────────── describe('CAD-1: dimLinearTool', () => { it('2 Klicks erzeugen dimension mit dimType, berechnetem value und Style', () => { const { ctx, doc } = mkCtx({ dimStyle: 'ISO-25', dimSubtype: 'horizontal' }); dimLinearTool.handlers.down!(pe(0, 0), ctx); dimLinearTool.handlers.down!(pe(100, 50), ctx); const els = doc.getAllElements(); expect(els).toHaveLength(1); const el = els[0]; expect(el.type).toBe('dimension'); expect(el.properties.dimType).toBe('linear'); expect(el.properties.dimSubtype).toBe('horizontal'); expect(el.properties.value).toBe('100'); // |dx| = 100 expect(el.properties.dimStyle).toBe('ISO-25'); }); it('vertikal: |dy| wird gemessen', () => { const { ctx, doc } = mkCtx({ dimSubtype: 'vertical' }); dimLinearTool.handlers.down!(pe(10, 0), ctx); dimLinearTool.handlers.down!(pe(20, 200), ctx); expect(doc.getAllElements()[0].properties.value).toBe('200'); }); it('horizontal missachtet y-Differenz', () => { const { ctx, doc } = mkCtx({ dimSubtype: 'horizontal' }); dimLinearTool.handlers.down!(pe(0, 0), ctx); dimLinearTool.handlers.down!(pe(100, 500), ctx); expect(doc.getAllElements()[0].properties.value).toBe('100'); }); it('Preview zeigt Bemassung waehrend Drag', () => { const { ctx, previews } = mkCtx(); dimLinearTool.handlers.down!(pe(0, 0), ctx); dimLinearTool.handlers.move!(pe(80, 30), ctx); expect(previews.length).toBeGreaterThanOrEqual(1); const last = previews[previews.length - 1]; expect(last!.type).toBe('dimension'); }); }); // ─── dimAlignedTool ──────────────────────────────────────── describe('CAD-1: dimAlignedTool', () => { it('misst Distanz entlang der Punkte-Verbindung (3-4-5)', () => { const { ctx, doc } = mkCtx(); dimAlignedTool.handlers.down!(pe(0, 0), ctx); dimAlignedTool.handlers.down!(pe(30, 40), ctx); const el = doc.getAllElements()[0]; expect(el.properties.dimType).toBe('aligned'); expect(el.properties.value).toBe('50'); }); }); // ─── dimAngularTool (3 Punkte: Scheitel + 2 Punkte) ─────── describe('CAD-1: dimAngularTool', () => { it('3 Klicks: 90 Grad zwischen zwei Schenkeln', () => { const { ctx, doc } = mkCtx(); dimAngularTool.handlers.down!(pe(0, 0), ctx); // Scheitel dimAngularTool.handlers.down!(pe(100, 0), ctx); // Schenkel 1 dimAngularTool.handlers.down!(pe(0, 100), ctx); // Schenkel 2 const el = doc.getAllElements()[0]; expect(el.properties.dimType).toBe('angular'); expect(el.properties.value).toBe('90\u00b0'); }); it('45 Grad wird korrekt gemessen', () => { const { ctx, doc } = mkCtx(); dimAngularTool.handlers.down!(pe(0, 0), ctx); dimAngularTool.handlers.down!(pe(100, 0), ctx); dimAngularTool.handlers.down!(pe(70.7, 70.7), ctx); // ~45 const el = doc.getAllElements()[0]; expect(el.properties.value).toContain('45'); }); }); // ─── dimRadiusTool / dimDiameterTool (Klick auf Kreis) ──── describe('CAD-1: dimRadiusTool + dimDiameterTool', () => { it('radius: Klick auf Kreis erzeugt R-Wert', () => { const { ctx, doc } = mkCtx(); const circle = { id: 'c1', type: 'circle', layerId: 'l', x: 100, y: 100, width: 50, height: 50, properties: { radius: 25 }, } as unknown as CADElement; doc.addElement(circle); // ctx.selection mocken (hitTest liefert den Kreis) ctx.selection = { hitTest: () => circle, getIds: () => [], setIds: () => {}, }; dimRadiusTool.handlers.down!(pe(100, 100), ctx); const el = doc.getAllElements().find((e) => e.type === 'dimension'); expect(el).toBeDefined(); expect(el!.properties.dimType).toBe('radius'); expect(el!.properties.value).toBe('R25'); }); it('diameter: Klick auf Kreis erzeugt Durchmesser-Wert', () => { const { ctx, doc } = mkCtx(); const circle = { id: 'c1', type: 'circle', layerId: 'l', x: 0, y: 0, width: 50, height: 50, properties: { radius: 25 }, } as unknown as CADElement; doc.addElement(circle); ctx.selection = { hitTest: () => circle, getIds: () => [], setIds: () => {}, }; dimDiameterTool.handlers.down!(pe(0, 0), ctx); const el = doc.getAllElements().find((e) => e.type === 'dimension'); expect(el!.properties.dimType).toBe('diameter'); expect(el!.properties.value).toContain('50'); }); it('Klick ohne Kreis-Treffer: kein Element, Status erklaert', () => { const { ctx, doc, statuses } = mkCtx(); ctx.selection = { hitTest: () => null, getIds: () => [], setIds: () => {} }; dimRadiusTool.handlers.down!(pe(0, 0), ctx); expect(doc.getAllElements()).toHaveLength(0); expect(statuses[statuses.length - 1]).toContain('Kreis'); }); }); // ─── RenderAdapter: komplette Bemassungs-Primitives ──────── describe('CAD-1: RenderAdapter mit Pfeilen', () => { it('dimension erzeugt jetzt MEHR Primitives (Extension+Masslinie+Text+Pfeile)', () => { const el = { id: 'd1', type: 'dimension', layerId: 'l', x: 50, y: -10, width: 100, height: 20, properties: { x1: 0, y1: 0, x2: 100, y2: 0, dimType: 'linear', dimSubtype: 'horizontal', value: '100', dimStyle: 'ISO-25', }, } as unknown as CADElement; const prims = elementToPrimitives(el); // Vorher: 2 Primitives (Linie + Text). Jetzt mit Extension lines + Pfeilen: mehr expect(prims.length).toBeGreaterThanOrEqual(5); const lines = prims.filter((p) => p.kind === 'line'); const texts = prims.filter((p) => p.kind === 'text'); const polys = prims.filter((p) => p.kind === 'polyline'); expect(lines.length).toBeGreaterThanOrEqual(3); // Masslinie + 2 Extension lines expect(texts.length).toBe(1); // Wert-Text expect(polys.length).toBeGreaterThanOrEqual(2); // 2 Pfeilspitzen }); it('Pfeilspitzen sind geschlossene Polygone an den Enden der Masslinie', () => { const el = { id: 'd1', type: 'dimension', layerId: 'l', x: 50, y: -10, width: 100, height: 20, properties: { x1: 0, y1: 0, x2: 100, y2: 0, dimType: 'linear', value: '100', }, } as unknown as CADElement; const prims = elementToPrimitives(el); const arrows = prims.filter( (p) => p.kind === 'polyline' && (p as { close?: boolean }).close === true, ); expect(arrows.length).toBe(2); }); it('radius-dimension rendert Linie vom Zentrum nach aussen + Pfeil', () => { const el = { id: 'd2', type: 'dimension', layerId: 'l', x: 100, y: 100, width: 0, height: 0, properties: { dimType: 'radius', value: 'R25', centerX: 100, centerY: 100, arrowX: 120, arrowY: 100, }, } as unknown as CADElement; const prims = elementToPrimitives(el); expect(prims.length).toBeGreaterThanOrEqual(2); expect(prims.some((p) => p.kind === 'line')).toBe(true); }); });