314 lines
12 KiB
TypeScript
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);
|
|
});
|
|
});
|