Files
web-cad/frontend/src/render/pixi/elementDrawers.ts
T

303 lines
9.4 KiB
TypeScript
Raw Normal View History

/**
* 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<string, unknown>;
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,
}];
}
case 'curtain': {
// Vorhang (Task E7): wellige Polyline entlang der Breite.
const pts = Array.isArray(props.points)
? (props.points as unknown[]).map(asPoint)
: [];
if (pts.length >= 2) {
return [{ kind: 'polyline', points: pts, close: false, stroke, strokeWidth: sw }];
}
return [{
kind: 'line',
from: { x: el.x - el.width / 2, y: el.y },
to: { x: el.x + el.width / 2, y: el.y },
stroke,
strokeWidth: sw,
}];
}
case 'spotlight': {
// Scheinwerfer (Task E7): Kegel-Polygon in properties.color.
const pts = Array.isArray(props.points)
? (props.points as unknown[]).map(asPoint)
: [];
if (pts.length >= 3) {
const col = (props.color as string | undefined) ?? '#ffcc00';
return [{ kind: 'polyline', points: pts, close: true, stroke: col, strokeWidth: sw }];
}
return [];
}
case 'barrier': {
// Absperrung (Task E7): Kette aus Kreis-Paaren (properties.circles).
const circles = Array.isArray(props.circles)
? (props.circles as Array<{ x: number; y: number; r: number }>)
: [];
const col = (props.color as string | undefined) ?? '#c0392b';
if (circles.length > 0) {
return circles.map((c) => ({
kind: 'circle' as const,
center: { x: c.x, y: c.y },
radius: c.r,
fill: null,
stroke: col,
strokeWidth: 1,
}));
}
const out: ShapePrimitive[] = [];
const N = 5;
for (let i = 0; i < N; i++) {
out.push({
kind: 'circle',
center: { x: el.x - el.width / 2 + (el.width * (i + 0.5)) / N, y: el.y },
radius: Math.min(6, el.width / (N * 2.5)),
fill: null,
stroke: col,
strokeWidth: 1,
});
}
return out;
}
default:
return [];
}
}