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

303 lines
9.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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 [];
}
}