Files
web-cad/frontend/tests/dimensionTools.test.ts
T

314 lines
12 KiB
TypeScript

/**
* 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<string, unknown> = {}) {
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);
});
});