/** * elementDrawers – Reine Beschreibungs-Fabrik (Task C2). * * Wandelt CADElemente in eine Liste von Primitiven um (line/rect/circle/ * polyline/text/arc). Der PixiRenderer (C3) konsumiert diese Deskriptoren * und überführt sie in konkrete Grafikobjekte. * * Vorteil dieser Zwischenschicht: * - Vollständig unit-testbar OHNE WebGL/GPU * - Backend-unabhängig (Pixi jetzt, Canvas2D oder andere später) * * Konventionen entsprechen dem Legacy RenderEngine.draw*: * - rect: x/y = Zentrum der BBox * - circle/arc: radius über properties.radius (Fallback width/2) * - arc-Winkel in Grad, 0° = 3 Uhr, gegen den Uhrzeigersinn */ import type { CADElement } from '../../types/cad.types'; import type { Pt } from '../../tools/modification/geometry'; /** Zeichenprimitiv — kleinste darstellbare Einheit. */ export type ShapePrimitive = | { kind: 'line'; from: Pt; to: Pt; stroke: string; strokeWidth: number } | { kind: 'rect'; x: number; y: number; w: number; h: number; fill: string | null; stroke: string; strokeWidth: number } | { kind: 'circle'; center: Pt; radius: number; fill: string | null; stroke: string; strokeWidth: number } | { kind: 'polyline'; points: Pt[]; close: boolean; stroke: string; strokeWidth: number } | { kind: 'text'; at: Pt; text: string; fontSize: number; color: string } | { kind: 'arc'; center: Pt; radius: number; startDeg: number; endDeg: number; stroke: string; strokeWidth: number }; const FALLBACK_STROKE = '#e0e0e0'; function asPoint(p: unknown): Pt { const q = p as { x?: number; y?: number }; return { x: Number(q?.x ?? 0), y: Number(q?.y ?? 0) }; } function fillOrNull(raw: unknown): string | null { return typeof raw === 'string' && raw !== 'none' ? raw : null; } /** * Wandelt ein CADElement in seine Zeichenprimitive um. * Gibt ein leeres Array zurück für unbekannte Typen bzw. Elemente, * die nichts Sichtbares beitragen (z.B. leerer Text). */ export function elementToPrimitives(el: CADElement): ShapePrimitive[] { const props = (el.properties ?? {}) as Record; const stroke = (props.stroke as string | undefined) ?? FALLBACK_STROKE; const sw = (props.strokeWidth as number | undefined) ?? 1; switch (el.type) { case 'line': { if ( props.x1 === undefined || props.y1 === undefined || props.x2 === undefined || props.y2 === undefined ) { return []; } return [{ kind: 'line', from: { x: props.x1 as number, y: props.y1 as number }, to: { x: props.x2 as number, y: props.y2 as number }, stroke, strokeWidth: sw, }]; } case 'rect': { // Legacy-BBox-Konvention: x/y ist das Zentrum des Rechtecks return [{ kind: 'rect', x: el.x - el.width / 2, y: el.y - el.height / 2, w: el.width, h: el.height, fill: fillOrNull(props.fill), stroke, strokeWidth: sw, }]; } case 'circle': { const radius = (props.radius as number | undefined) ?? el.width / 2; return [{ kind: 'circle', center: { x: el.x, y: el.y }, radius, fill: fillOrNull(props.fill), stroke, strokeWidth: sw, }]; } case 'arc': { const radius = (props.radius as number | undefined) ?? el.width / 2; return [{ kind: 'arc', center: { x: el.x, y: el.y }, radius, startDeg: (props.startAngle as number | undefined) ?? 0, endDeg: (props.endAngle as number | undefined) ?? 360, stroke, strokeWidth: sw, }]; } case 'polyline': case 'polygon': { const pts = Array.isArray(props.points) ? (props.points as unknown[]).map(asPoint) : []; if (pts.length < 2) return []; return [{ kind: 'polyline', points: pts, close: el.type === 'polygon', stroke, strokeWidth: sw, }]; } case 'text': { const txt = String(props.text ?? ''); if (!txt) return []; // leeren Platzhalter nicht rendern return [{ kind: 'text', at: { x: el.x, y: el.y }, text: txt, fontSize: (props.fontSize as number | undefined) ?? 12, color: (props.color as string | undefined) ?? stroke, }]; } // ── Task C3: bisher im Pixi-Pfad fehlende Typen ── case 'dimension': { // Legacy-Konvention: x1..y2 in properties, BBox-Zentrum als x/y, // Extension lines + Hauptlinie; Pfeilspitzen sind Detail (C3+) const x1 = (props.x1 as number | undefined) ?? el.x - el.width / 2; const y1 = (props.y1 as number | undefined) ?? el.y; const x2 = (props.x2 as number | undefined) ?? el.x + el.width / 2; const y2 = (props.y2 as number | undefined) ?? el.y; const color = (props.color as string | undefined) ?? '#aaaaaa'; const dimColor = (props.dimColor as string | undefined) ?? color; const label = String(props.value ?? props.text ?? ''); const out: ShapePrimitive[] = [ { kind: 'line', from: { x: x1, y: y1 }, to: { x: x2, y: y2 }, stroke: dimColor, strokeWidth: 1 }, ]; if (label) { out.push({ kind: 'text', at: { x: (x1 + x2) / 2, y: (y1 + y2) / 2 - 12 }, text: label, fontSize: 10, color: dimColor, }); } return out; } case 'leader': { // Legacy-Konvention: Linie p1→p2 + optionaler Text am Ende const lx1 = (props.x1 as number | undefined) ?? el.x; const ly1 = (props.y1 as number | undefined) ?? el.y; const lx2 = (props.x2 as number | undefined) ?? el.x + el.width; const ly2 = (props.y2 as number | undefined) ?? el.y + el.height; const out: ShapePrimitive[] = [ { kind: 'line', from: { x: lx1, y: ly1 }, to: { x: lx2, y: ly2 }, stroke, strokeWidth: sw }, ]; const label = String(props.text ?? ''); if (label) { out.push({ kind: 'text', at: { x: lx2 + 4, y: ly2 - 4 }, text: label, fontSize: (props.fontSize as number | undefined) ?? 12, color: stroke, }); } return out; } case 'revcloud': { // Revisionswolke: Punkte-Zug (Bogen-Detail folgt in C3+) const pts = Array.isArray(props.points) ? (props.points as unknown[]).map(asPoint) : []; if (pts.length < 2) return []; return [{ kind: 'polyline', points: pts, close: true, stroke, strokeWidth: sw, }]; } case 'chair': { // Stuhl: gefülltes Quadrat in Element-Farbe + Umrandung const fill = (props.fill as string | undefined) ?? '#4a90d9'; return [{ kind: 'rect', x: el.x - el.width / 2, y: el.y - el.height / 2, w: el.width, h: el.height, fill, stroke: (props.outlineColor as string | undefined) ?? '#2a5a99', strokeWidth: 1, }]; } case 'table': { // Tisch: Kreis in Element-Größe mit Füllung const fill = (props.fill as string | undefined) ?? '#8b7355'; return [{ kind: 'circle', center: { x: el.x, y: el.y }, radius: Math.max(el.width, el.height) / 2, fill, stroke: '#5a4a35', strokeWidth: 1, }]; } case 'stage': { // Bühne: Rechteck mit dunkler Füllung return [{ kind: 'rect', x: el.x - el.width / 2, y: el.y - el.height / 2, w: el.width, h: el.height, fill: (props.fill as string | undefined) ?? '#3a3a4a', stroke, strokeWidth: 1.5, }]; } default: return []; } }