303 lines
9.4 KiB
TypeScript
303 lines
9.4 KiB
TypeScript
/**
|
||
* 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 [];
|
||
}
|
||
}
|