task(CAD-1): associative dimensioning - DimStyles (ISO-25/50/Arch/Engineering), 5 dimension types (linear H/V, aligned, angular, radius, diameter), computed values, full primitives with arrows + extension lines
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user