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:
@@ -6,6 +6,9 @@
|
|||||||
* (2-Punkt Pfeil+Label).
|
* (2-Punkt Pfeil+Label).
|
||||||
*/
|
*/
|
||||||
import type { PluginV2, ToolExtensionV2, FullToolContext } from '../../types';
|
import type { PluginV2, ToolExtensionV2, FullToolContext } from '../../types';
|
||||||
|
import {
|
||||||
|
DEFAULT_DIM_STYLE, getDimStyle, computeDimValue, formatDimValue,
|
||||||
|
} from '../../../services/dimStyleService';
|
||||||
import type { CADElement } from '../../../types/cad.types';
|
import type { CADElement } from '../../../types/cad.types';
|
||||||
import type { Pt } from '../../../tools/modification/geometry';
|
import type { Pt } from '../../../tools/modification/geometry';
|
||||||
|
|
||||||
@@ -193,6 +196,197 @@ export const mtextTool: ToolExtensionV2 = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Task CAD-1: Assoziative Bemassung — 5 Typen mit DimStyles.
|
||||||
|
// linear (H/V), aligned, angular (3 Klicks), radius, diameter.
|
||||||
|
// Alle berechnen den Masswert aus der Geometrie (assoziativ).
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let dimStart: Pt | null = null;
|
||||||
|
let dimVertex: Pt | null = null; // angular: Scheitel
|
||||||
|
|
||||||
|
function dimStyleOf(ctx: FullToolContext) {
|
||||||
|
const name = (ctx.options.dimStyle as string | undefined) ?? 'ISO-25';
|
||||||
|
return getDimStyle(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitDim(el: CADElement, ctx: FullToolContext, noun: string): void {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc) return;
|
||||||
|
c.doc.transact(() => c.doc!.addElement(el));
|
||||||
|
ctx.setPreview?.(null);
|
||||||
|
ctx.setStatus(`${noun} erstellt: ${String(el.properties.value)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dimLinearTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'dim-linear', label: 'Linear-Bemassung', icon: '\u2194',
|
||||||
|
ribbonTab: 'format', tags: ['basic'],
|
||||||
|
description: 'Lineare Bemassung: 2 Punkte, horizontal/vertikal (Option)',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'dimStyle', label: 'Stil', type: 'select',
|
||||||
|
options: [{ value: 'ISO-25', label: 'ISO-25' }, { value: 'ISO-50', label: 'ISO-50' },
|
||||||
|
{ value: 'Architectural', label: 'Architektur' }, { value: 'Engineering', label: 'Engineering' }] },
|
||||||
|
{ key: 'dimSubtype', label: 'Ausrichtung', type: 'select',
|
||||||
|
options: [{ value: 'horizontal', label: 'Horizontal' }, { value: 'vertical', label: 'Vertikal' }] },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc) return;
|
||||||
|
if (!dimStart) { dimStart = e.world; ctx.setStatus('Bemassung: zweiten Punkt wählen'); return; }
|
||||||
|
const subtype = (c.options.dimSubtype as string | undefined) ?? 'horizontal';
|
||||||
|
const style = dimStyleOf(c);
|
||||||
|
const raw = computeDimValue('linear', dimStart, e.world, undefined, subtype);
|
||||||
|
const el: CADElement = {
|
||||||
|
id: uid('dim'), type: 'dimension', layerId: (c.options.layerId as string | undefined) ?? 'layer-0',
|
||||||
|
x: (dimStart.x + e.world.x) / 2, y: (dimStart.y + e.world.y) / 2,
|
||||||
|
width: Math.abs(e.world.x - dimStart.x), height: Math.abs(e.world.y - dimStart.y),
|
||||||
|
properties: {
|
||||||
|
x1: dimStart.x, y1: dimStart.y, x2: e.world.x, y2: e.world.y,
|
||||||
|
dimType: 'linear', dimSubtype: subtype,
|
||||||
|
value: formatDimValue(raw, style), dimStyle: style.name,
|
||||||
|
},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
dimStart = null;
|
||||||
|
commitDim(el, ctx, 'Linearbemassung');
|
||||||
|
},
|
||||||
|
move(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!dimStart) return;
|
||||||
|
const subtype = (c.options.dimSubtype as string | undefined) ?? 'horizontal';
|
||||||
|
const style = dimStyleOf(c);
|
||||||
|
const raw = computeDimValue('linear', dimStart, e.world, undefined, subtype);
|
||||||
|
ctx.setPreview?.({
|
||||||
|
id: '__preview_dim__', type: 'dimension', layerId: 'layer-0',
|
||||||
|
x: (dimStart.x + e.world.x) / 2, y: (dimStart.y + e.world.y) / 2,
|
||||||
|
width: Math.abs(e.world.x - dimStart.x), height: Math.abs(e.world.y - dimStart.y),
|
||||||
|
properties: {
|
||||||
|
x1: dimStart.x, y1: dimStart.y, x2: e.world.x, y2: e.world.y,
|
||||||
|
dimType: 'linear', dimSubtype: subtype,
|
||||||
|
value: formatDimValue(raw, style), dimStyle: style.name,
|
||||||
|
},
|
||||||
|
} as unknown as CADElement);
|
||||||
|
},
|
||||||
|
cancel(ctx) { dimStart = null; ctx.setPreview?.(null); ctx.setStatus('Bemassung abgebrochen'); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dimAlignedTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'dim-aligned', label: 'Ausgerichtet', icon: '\u2197',
|
||||||
|
ribbonTab: 'format', tags: ['basic'],
|
||||||
|
description: 'Bemassung entlang der Punkte-Verbindung',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'dimStyle', label: 'Stil', type: 'select',
|
||||||
|
options: [{ value: 'ISO-25', label: 'ISO-25' }, { value: 'ISO-50', label: 'ISO-50' }] },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc) return;
|
||||||
|
if (!dimStart) { dimStart = e.world; ctx.setStatus('Ausgerichtet: zweiten Punkt wählen'); return; }
|
||||||
|
const style = dimStyleOf(c);
|
||||||
|
const raw = computeDimValue('aligned', dimStart, e.world);
|
||||||
|
const el: CADElement = {
|
||||||
|
id: uid('dim'), type: 'dimension', layerId: (c.options.layerId as string | undefined) ?? 'layer-0',
|
||||||
|
x: (dimStart.x + e.world.x) / 2, y: (dimStart.y + e.world.y) / 2,
|
||||||
|
width: Math.abs(e.world.x - dimStart.x), height: Math.abs(e.world.y - dimStart.y),
|
||||||
|
properties: {
|
||||||
|
x1: dimStart.x, y1: dimStart.y, x2: e.world.x, y2: e.world.y,
|
||||||
|
dimType: 'aligned', value: formatDimValue(raw, style), dimStyle: style.name,
|
||||||
|
},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
dimStart = null;
|
||||||
|
commitDim(el, ctx, 'Ausgerichtete Bemassung');
|
||||||
|
},
|
||||||
|
cancel(ctx) { dimStart = null; ctx.setPreview?.(null); ctx.setStatus('Abgebrochen'); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const dimAngularTool: ToolExtensionV2 = {
|
||||||
|
manifest: {
|
||||||
|
id: 'dim-angular', label: 'Winkel', icon: '\u2220',
|
||||||
|
ribbonTab: 'format', tags: ['pro'],
|
||||||
|
description: 'Winkelbemassung: Scheitel, Schenkel 1, Schenkel 2',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'dimStyle', label: 'Stil', type: 'select',
|
||||||
|
options: [{ value: 'ISO-25', label: 'ISO-25' }] },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc) return;
|
||||||
|
if (!dimVertex) { dimVertex = e.world; ctx.setStatus('Winkel: Schenkel 1 klicken'); return; }
|
||||||
|
if (!dimStart) { dimStart = e.world; ctx.setStatus('Winkel: Schenkel 2 klicken'); return; }
|
||||||
|
const style = dimStyleOf(c);
|
||||||
|
const leg1 = { x: dimStart.x - dimVertex.x, y: dimStart.y - dimVertex.y };
|
||||||
|
const leg2 = { x: e.world.x - dimVertex.x, y: e.world.y - dimVertex.y };
|
||||||
|
const raw = computeDimValue('angular', leg1, leg2);
|
||||||
|
const el: CADElement = {
|
||||||
|
id: uid('dim'), type: 'dimension', layerId: (c.options.layerId as string | undefined) ?? 'layer-0',
|
||||||
|
x: dimVertex.x, y: dimVertex.y, width: 0, height: 0,
|
||||||
|
properties: {
|
||||||
|
dimType: 'angular', value: formatDimValue(raw, style, 'angular'),
|
||||||
|
vertexX: dimVertex.x, vertexY: dimVertex.y,
|
||||||
|
leg1X: dimStart.x, leg1Y: dimStart.y, leg2X: e.world.x, leg2Y: e.world.y,
|
||||||
|
dimStyle: style.name,
|
||||||
|
},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
dimVertex = null; dimStart = null;
|
||||||
|
commitDim(el, ctx, 'Winkelbemassung');
|
||||||
|
},
|
||||||
|
cancel(ctx) { dimVertex = null; dimStart = null; ctx.setPreview?.(null); ctx.setStatus('Abgebrochen'); },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function makeRadialDim(mode: 'radius' | 'diameter'): ToolExtensionV2 {
|
||||||
|
return {
|
||||||
|
manifest: {
|
||||||
|
id: mode === 'radius' ? 'dim-radius' : 'dim-diameter',
|
||||||
|
label: mode === 'radius' ? 'Radius' : 'Durchmesser',
|
||||||
|
icon: mode === 'radius' ? '\u2199' : '\u2300',
|
||||||
|
ribbonTab: 'format', tags: ['basic'],
|
||||||
|
description: mode === 'radius' ? 'Radius bemassen (Kreis anklicken)' : 'Durchmesser bemassen (Kreis anklicken)',
|
||||||
|
},
|
||||||
|
optionsSchema: [
|
||||||
|
{ key: 'dimStyle', label: 'Stil', type: 'select',
|
||||||
|
options: [{ value: 'ISO-25', label: 'ISO-25' }] },
|
||||||
|
],
|
||||||
|
handlers: {
|
||||||
|
down(e, ctx) {
|
||||||
|
const c = ctx as FullToolContext;
|
||||||
|
if (!c.doc || !c.selection) return;
|
||||||
|
const hit = c.selection.hitTest(e.world.x, e.world.y, 8);
|
||||||
|
if (!hit || hit.type !== 'circle') {
|
||||||
|
ctx.setStatus(`${mode === 'radius' ? 'Radius' : 'Durchmesser'}: Kreis anklicken`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const style = dimStyleOf(c);
|
||||||
|
const raw = computeDimValue(mode, undefined, undefined, hit);
|
||||||
|
const el: CADElement = {
|
||||||
|
id: uid('dim'), type: 'dimension', layerId: hit.layerId,
|
||||||
|
x: hit.x, y: hit.y, width: hit.width, height: hit.height,
|
||||||
|
properties: {
|
||||||
|
dimType: mode, value: formatDimValue(raw, style, mode),
|
||||||
|
centerX: hit.x, centerY: hit.y,
|
||||||
|
arrowX: hit.x + ((hit.properties as Record<string, unknown>).radius as number ?? hit.width / 2),
|
||||||
|
arrowY: hit.y,
|
||||||
|
dimStyle: style.name,
|
||||||
|
},
|
||||||
|
} as unknown as CADElement;
|
||||||
|
commitDim(el, ctx, mode === 'radius' ? 'Radiusbemassung' : 'Durchmesserbemassung');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dimRadiusTool: ToolExtensionV2 = makeRadialDim('radius');
|
||||||
|
export const dimDiameterTool: ToolExtensionV2 = makeRadialDim('diameter');
|
||||||
|
|
||||||
export const coreAnnotatePlugin: PluginV2 = {
|
export const coreAnnotatePlugin: PluginV2 = {
|
||||||
manifest: {
|
manifest: {
|
||||||
id: 'core-annotate',
|
id: 'core-annotate',
|
||||||
@@ -203,5 +397,5 @@ export const coreAnnotatePlugin: PluginV2 = {
|
|||||||
category: 'tools',
|
category: 'tools',
|
||||||
enabledByDefault: true,
|
enabledByDefault: true,
|
||||||
},
|
},
|
||||||
tools: [textTool, dimensionTool, leaderTool, mtextTool],
|
tools: [textTool, dimensionTool, leaderTool, mtextTool, dimLinearTool, dimAlignedTool, dimAngularTool, dimRadiusTool, dimDiameterTool],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
* - arc-Winkel in Grad, 0° = 3 Uhr, gegen den Uhrzeigersinn
|
* - arc-Winkel in Grad, 0° = 3 Uhr, gegen den Uhrzeigersinn
|
||||||
*/
|
*/
|
||||||
import type { CADElement } from '../../types/cad.types';
|
import type { CADElement } from '../../types/cad.types';
|
||||||
|
import { getDimStyle } from '../../services/dimStyleService';
|
||||||
import type { Pt } from '../../tools/modification/geometry';
|
import type { Pt } from '../../tools/modification/geometry';
|
||||||
|
|
||||||
/** Zeichenprimitiv — kleinste darstellbare Einheit. */
|
/** Zeichenprimitiv — kleinste darstellbare Einheit. */
|
||||||
@@ -42,6 +43,29 @@ function fillOrNull(raw: unknown): string | null {
|
|||||||
* Gibt ein leeres Array zurück für unbekannte Typen bzw. Elemente,
|
* Gibt ein leeres Array zurück für unbekannte Typen bzw. Elemente,
|
||||||
* die nichts Sichtbares beitragen (z.B. leerer Text).
|
* die nichts Sichtbares beitragen (z.B. leerer Text).
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Pfeilspitze als geschlossenes Polygon (Task CAD-1).
|
||||||
|
* Spitze bei (x,y), Richtung ang (rad), Laenge size.
|
||||||
|
*/
|
||||||
|
function arrowPrimitives(x: number, y: number, ang: number, size: number, color: string): ShapePrimitive[] {
|
||||||
|
const halfW = size * 0.35;
|
||||||
|
const bx = x - size * Math.cos(ang);
|
||||||
|
const by = y - size * Math.sin(ang);
|
||||||
|
const px = -Math.sin(ang) * halfW;
|
||||||
|
const py = Math.cos(ang) * halfW;
|
||||||
|
return [{
|
||||||
|
kind: 'polyline',
|
||||||
|
points: [
|
||||||
|
{ x, y },
|
||||||
|
{ x: bx + px, y: by + py },
|
||||||
|
{ x: bx - px, y: by - py },
|
||||||
|
],
|
||||||
|
close: true,
|
||||||
|
stroke: color,
|
||||||
|
strokeWidth: 1,
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: string; prompt: string; defaultValue?: string }>): ShapePrimitive[] {
|
export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: string; prompt: string; defaultValue?: string }>): ShapePrimitive[] {
|
||||||
const props = (el.properties ?? {}) as Record<string, unknown>;
|
const props = (el.properties ?? {}) as Record<string, unknown>;
|
||||||
const stroke = (props.stroke as string | undefined) ?? FALLBACK_STROKE;
|
const stroke = (props.stroke as string | undefined) ?? FALLBACK_STROKE;
|
||||||
@@ -133,25 +157,75 @@ export function elementToPrimitives(el: CADElement, attrDefs?: Array<{ tag: stri
|
|||||||
// ── Task C3: bisher im Pixi-Pfad fehlende Typen ──
|
// ── Task C3: bisher im Pixi-Pfad fehlende Typen ──
|
||||||
|
|
||||||
case 'dimension': {
|
case 'dimension': {
|
||||||
// Legacy-Konvention: x1..y2 in properties, BBox-Zentrum als x/y,
|
// Task CAD-1: komplette Bemassung mit Extension lines, Masslinie,
|
||||||
// Extension lines + Hauptlinie; Pfeilspitzen sind Detail (C3+)
|
// Pfeilspitzen (geschlossene Polygone) und Wert-Text nach DimStyle.
|
||||||
|
const dimType = (props.dimType as string | undefined) ?? 'linear';
|
||||||
|
const style = getDimStyle(props.dimStyle as string | undefined);
|
||||||
|
const dimColor = (props.dimColor as string | undefined) ?? (props.color as string | undefined) ?? '#aaaaaa';
|
||||||
|
const label = String(props.value ?? props.text ?? '');
|
||||||
|
|
||||||
|
if (dimType === 'radius' || dimType === 'diameter') {
|
||||||
|
const cx = (props.centerX as number | undefined) ?? el.x;
|
||||||
|
const cy = (props.centerY as number | undefined) ?? el.y;
|
||||||
|
const ax = (props.arrowX as number | undefined) ?? cx + el.width / 2;
|
||||||
|
const ay = (props.arrowY as number | undefined) ?? cy;
|
||||||
|
const out: ShapePrimitive[] = [
|
||||||
|
{ kind: 'line', from: { x: cx, y: cy }, to: { x: ax, y: ay }, stroke: dimColor, strokeWidth: 1 },
|
||||||
|
];
|
||||||
|
out.push(...arrowPrimitives(ax, ay, Math.atan2(ay - cy, ax - cx), style.arrowSize, dimColor));
|
||||||
|
if (label) {
|
||||||
|
out.push({ kind: 'text', at: { x: (cx + ax) / 2, y: (cy + ay) / 2 - style.textHeight * 0.6 }, text: label, fontSize: style.textHeight * 4, color: dimColor });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dimType === 'angular') {
|
||||||
|
const vx = (props.vertexX as number | undefined) ?? el.x;
|
||||||
|
const vy = (props.vertexY as number | undefined) ?? el.y;
|
||||||
|
const l1x = (props.leg1X as number | undefined) ?? vx + 100;
|
||||||
|
const l1y = (props.leg1Y as number | undefined) ?? vy;
|
||||||
|
const l2x = (props.leg2X as number | undefined) ?? vx;
|
||||||
|
const l2y = (props.leg2Y as number | undefined) ?? vy + 100;
|
||||||
|
const out: ShapePrimitive[] = [
|
||||||
|
{ kind: 'line', from: { x: vx, y: vy }, to: { x: l1x, y: l1y }, stroke: dimColor, strokeWidth: 1 },
|
||||||
|
{ kind: 'line', from: { x: vx, y: vy }, to: { x: l2x, y: l2y }, stroke: dimColor, strokeWidth: 1 },
|
||||||
|
];
|
||||||
|
if (label) {
|
||||||
|
out.push({ kind: 'text', at: { x: vx + 30, y: vy - 10 }, text: label, fontSize: style.textHeight * 4, color: dimColor });
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
// linear / aligned
|
||||||
const x1 = (props.x1 as number | undefined) ?? el.x - el.width / 2;
|
const x1 = (props.x1 as number | undefined) ?? el.x - el.width / 2;
|
||||||
const y1 = (props.y1 as number | undefined) ?? el.y;
|
const y1 = (props.y1 as number | undefined) ?? el.y;
|
||||||
const x2 = (props.x2 as number | undefined) ?? el.x + el.width / 2;
|
const x2 = (props.x2 as number | undefined) ?? el.x + el.width / 2;
|
||||||
const y2 = (props.y2 as number | undefined) ?? el.y;
|
const y2 = (props.y2 as number | undefined) ?? el.y;
|
||||||
const color = (props.color as string | undefined) ?? '#aaaaaa';
|
const subtype = (props.dimSubtype as string | undefined) ?? 'horizontal';
|
||||||
const dimColor = (props.dimColor as string | undefined) ?? color;
|
|
||||||
const label = String(props.value ?? props.text ?? '');
|
// Masslinie: bei horizontal bei y = min(y1,y2) - offset; vertical bei x analog
|
||||||
|
const off = 15;
|
||||||
|
let mx1 = x1, my1 = y1, mx2 = x2, my2 = y2;
|
||||||
|
if (subtype === 'horizontal') { my1 = Math.min(y1, y2) - off; my2 = my1; }
|
||||||
|
else if (subtype === 'vertical') { mx1 = Math.min(x1, x2) - off; mx2 = mx1; }
|
||||||
|
else { /* aligned: Masslinie = Punkte-Verbindung */ }
|
||||||
|
|
||||||
const out: ShapePrimitive[] = [
|
const out: ShapePrimitive[] = [
|
||||||
{ kind: 'line', from: { x: x1, y: y1 }, to: { x: x2, y: y2 }, stroke: dimColor, strokeWidth: 1 },
|
// Extension lines
|
||||||
|
{ kind: 'line', from: { x: x1, y: y1 }, to: { x: x1, y: my1 }, stroke: '#888888', strokeWidth: 0.5 },
|
||||||
|
{ kind: 'line', from: { x: x2, y: y2 }, to: { x: x2, y: my2 }, stroke: '#888888', strokeWidth: 0.5 },
|
||||||
|
// Masslinie
|
||||||
|
{ kind: 'line', from: { x: mx1, y: my1 }, to: { x: mx2, y: my2 }, stroke: dimColor, strokeWidth: 1 },
|
||||||
];
|
];
|
||||||
|
// Pfeilspitzen an beiden Enden der Masslinie
|
||||||
|
const ang1 = Math.atan2(my2 - my1, mx2 - mx1);
|
||||||
|
out.push(...arrowPrimitives(mx1, my1, ang1, style.arrowSize, dimColor));
|
||||||
|
out.push(...arrowPrimitives(mx2, my2, ang1 + Math.PI, style.arrowSize, dimColor));
|
||||||
if (label) {
|
if (label) {
|
||||||
out.push({
|
out.push({
|
||||||
kind: 'text',
|
kind: 'text',
|
||||||
at: { x: (x1 + x2) / 2, y: (y1 + y2) / 2 - 12 },
|
at: { x: (mx1 + mx2) / 2, y: (my1 + my2) / 2 - style.textHeight * 0.6 },
|
||||||
text: label,
|
text: label, fontSize: style.textHeight * 4, color: dimColor,
|
||||||
fontSize: 10,
|
|
||||||
color: dimColor,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/**
|
||||||
|
* Task CAD-1 – Bemassungsstile (DimStyles) + assoziative Wert-Berechnung.
|
||||||
|
*
|
||||||
|
* DimStyle: ISO-artige Stile (Text-/Pfeilhoehe, Einheit, Dezimalstellen,
|
||||||
|
* Pfeilstil, Linien-Offsets). computeDimValue berechnet den Masswert
|
||||||
|
* aus der Geometrie (assoziativ), formatDimValue erzeugt den Text.
|
||||||
|
*/
|
||||||
|
import type { CADElement } from '../types/cad.types';
|
||||||
|
import type { Pt } from '../tools/modification/geometry';
|
||||||
|
|
||||||
|
export type DimTypeName = 'linear' | 'aligned' | 'angular' | 'radius' | 'diameter';
|
||||||
|
|
||||||
|
export interface DimStyle {
|
||||||
|
name: string;
|
||||||
|
/** Text-Hoehe in Welt-Einheiten. */
|
||||||
|
textHeight: number;
|
||||||
|
/** Pfeil-Laenge in Welt-Einheiten. */
|
||||||
|
arrowSize: number;
|
||||||
|
unit: 'mm' | 'cm' | 'm';
|
||||||
|
decimals: number;
|
||||||
|
arrowStyle: 'closed' | 'open' | 'dot' | 'tick';
|
||||||
|
/** Offset Extension-Linie zu Messpunkt. */
|
||||||
|
extOffset: number;
|
||||||
|
/** Ueberstand der Masslinie ueber Pfeile. */
|
||||||
|
overshoot: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_DIM_STYLE: DimStyle = {
|
||||||
|
name: 'ISO-25',
|
||||||
|
textHeight: 2.5,
|
||||||
|
arrowSize: 2.5,
|
||||||
|
unit: 'mm',
|
||||||
|
decimals: 0,
|
||||||
|
arrowStyle: 'closed',
|
||||||
|
extOffset: 0.625,
|
||||||
|
overshoot: 1.25,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DIM_STYLES: DimStyle[] = [
|
||||||
|
DEFAULT_DIM_STYLE,
|
||||||
|
{ ...DEFAULT_DIM_STYLE, name: 'ISO-50', textHeight: 5, arrowSize: 5 },
|
||||||
|
{ ...DEFAULT_DIM_STYLE, name: 'Architectural', textHeight: 3, arrowSize: 2, arrowStyle: 'tick' },
|
||||||
|
{ ...DEFAULT_DIM_STYLE, name: 'Engineering', textHeight: 2.5, arrowSize: 2.5, decimals: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
/** Stil per Name (Fallback: Default). */
|
||||||
|
export function getDimStyle(name: string | undefined): DimStyle {
|
||||||
|
if (!name) return DEFAULT_DIM_STYLE;
|
||||||
|
return DIM_STYLES.find((s) => s.name === name) ?? DEFAULT_DIM_STYLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assoziative Masswert-Berechnung aus der Geometrie.
|
||||||
|
* linear: |dx| (horizontal) bzw. |dy| (vertikal) — der Aufrufer waehlt
|
||||||
|
* die Achse; hier wird die Gesamt-Projektion geliefert und das
|
||||||
|
* Tool waehlt horizontal/vertikal anhand dimSubtype.
|
||||||
|
* aligned: Distanz zwischen p1 und p2.
|
||||||
|
* angular: Winkel zwischen zwei Schenkeln am Scheitel (Grad).
|
||||||
|
* radius/diameter: aus einem Kreis-Element.
|
||||||
|
*/
|
||||||
|
export function computeDimValue(
|
||||||
|
type: DimTypeName,
|
||||||
|
p1?: Pt,
|
||||||
|
p2?: Pt,
|
||||||
|
element?: CADElement,
|
||||||
|
subtype?: string,
|
||||||
|
): number {
|
||||||
|
if (type === 'linear' && p1 && p2) {
|
||||||
|
if (subtype === 'vertical') return Math.abs(p2.y - p1.y);
|
||||||
|
if (subtype === 'horizontal') return Math.abs(p2.x - p1.x);
|
||||||
|
// Ohne explizites Subtype: dominante Achse (CAD-Standardverhalten)
|
||||||
|
return Math.abs(p2.x - p1.x) >= Math.abs(p2.y - p1.y)
|
||||||
|
? Math.abs(p2.x - p1.x)
|
||||||
|
: Math.abs(p2.y - p1.y);
|
||||||
|
}
|
||||||
|
if (type === 'aligned' && p1 && p2) {
|
||||||
|
return Math.hypot(p2.x - p1.x, p2.y - p1.y);
|
||||||
|
}
|
||||||
|
if (type === 'radius' && element) {
|
||||||
|
const r = (element.properties as Record<string, unknown>).radius as number | undefined;
|
||||||
|
return r ?? element.width / 2;
|
||||||
|
}
|
||||||
|
if (type === 'diameter' && element) {
|
||||||
|
const r = (element.properties as Record<string, unknown>).radius as number | undefined;
|
||||||
|
return (r ?? element.width / 2) * 2;
|
||||||
|
}
|
||||||
|
if (type === 'angular' && p1 && p2) {
|
||||||
|
const ang1 = Math.atan2(p1.y, p1.x);
|
||||||
|
const ang2 = Math.atan2(p2.y, p2.x);
|
||||||
|
let diff = Math.abs(ang2 - ang1) * 180 / Math.PI;
|
||||||
|
if (diff > 180) diff = 360 - diff;
|
||||||
|
return diff;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Formatiert einen Masswert nach DimStyle (mit Typ-Praefix). */
|
||||||
|
export function formatDimValue(
|
||||||
|
value: number,
|
||||||
|
style: DimStyle,
|
||||||
|
type?: DimTypeName,
|
||||||
|
): string {
|
||||||
|
const num = value.toFixed(style.decimals);
|
||||||
|
switch (type) {
|
||||||
|
case 'radius': return `R${num}`;
|
||||||
|
case 'diameter': return `\u2300 ${num}`;
|
||||||
|
case 'angular': return `${num}\u00b0`;
|
||||||
|
default: return num;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -113,14 +113,16 @@ describe('elementToPrimitives', () => {
|
|||||||
expect(elementToPrimitives(el('x', 'definitely-unknown'))).toEqual([]);
|
expect(elementToPrimitives(el('x', 'definitely-unknown'))).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dimension: Linie + Label aus value/text', () => {
|
it('dimension: komplette Bemassung (Extension+Masslinie+Pfeile+Text, CAD-1)', () => {
|
||||||
const prims = elementToPrimitives(el('d', 'dimension', {
|
const prims = elementToPrimitives(el('d', 'dimension', {
|
||||||
x1: 0, y1: 0, x2: 100, y2: 0, value: '250 cm',
|
x1: 0, y1: 0, x2: 100, y2: 0, value: '250 cm',
|
||||||
}, { x: 50, y: 0 }));
|
}, { x: 50, y: 0 }));
|
||||||
expect(prims.length).toBe(2);
|
// CAD-1: 2 Extension lines + 1 Masslinie + 2 Pfeilspitzen + 1 Text = 6
|
||||||
expect(prims[0].kind).toBe('line');
|
expect(prims.length).toBe(6);
|
||||||
const txt = prims[1];
|
expect(prims.filter((p) => p.kind === 'line').length).toBe(3);
|
||||||
if (txt.kind !== 'text') throw new Error('erwartete text');
|
expect(prims.filter((p) => p.kind === 'polyline').length).toBe(2);
|
||||||
|
const txt = prims.find((p) => p.kind === 'text');
|
||||||
|
if (!txt || txt.kind !== 'text') throw new Error('erwartete text');
|
||||||
expect(txt.text).toBe('250 cm');
|
expect(txt.text).toBe('250 cm');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user