2026-08-27 08:49:18 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* 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,
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 10:07:35 +02:00
|
|
|
|
// ── 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,
|
|
|
|
|
|
}];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-29 02:36:20 +02:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 08:49:18 +02:00
|
|
|
|
default:
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|