diff --git a/frontend/src/render/pixi/elementDrawers.ts b/frontend/src/render/pixi/elementDrawers.ts index f979eda..04b0682 100644 --- a/frontend/src/render/pixi/elementDrawers.ts +++ b/frontend/src/render/pixi/elementDrawers.ts @@ -130,6 +130,112 @@ export function elementToPrimitives(el: CADElement): ShapePrimitive[] { }]; } + // ── 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 []; } diff --git a/frontend/tests/elementDrawers.test.ts b/frontend/tests/elementDrawers.test.ts index f3b87c4..12dfde2 100644 --- a/frontend/tests/elementDrawers.test.ts +++ b/frontend/tests/elementDrawers.test.ts @@ -112,4 +112,63 @@ describe('elementToPrimitives', () => { it('unbekannter Typ ergibt leere Liste (renderer-sicher)', () => { expect(elementToPrimitives(el('x', 'definitely-unknown'))).toEqual([]); }); + + it('dimension: Linie + Label aus value/text', () => { + const prims = elementToPrimitives(el('d', 'dimension', { + x1: 0, y1: 0, x2: 100, y2: 0, value: '250 cm', + }, { x: 50, y: 0 })); + expect(prims.length).toBe(2); + expect(prims[0].kind).toBe('line'); + const txt = prims[1]; + if (txt.kind !== 'text') throw new Error('erwartete text'); + expect(txt.text).toBe('250 cm'); + }); + + it('leader: Linie + Label am Ende', () => { + const prims = elementToPrimitives(el('ld', 'leader', { + x1: 10, y1: 10, x2: 80, y2: 60, text: 'A1', fontSize: 14, + })); + expect(prims.length).toBe(2); + expect(prims[0].kind).toBe('line'); + const txt = prims[1]; + if (txt.kind !== 'text') throw new Error('erwartete text'); + expect(txt.text).toBe('A1'); + expect(txt.fontSize).toBe(14); + }); + + it('revcloud: geschlossener Punktzug', () => { + const prims = elementToPrimitives(el('rc', 'revcloud', { + points: [{ x: 0, y: 0 }, { x: 50, y: 0 }, { x: 50, y: 30 }], + })); + expect(prims.length).toBe(1); + const p = prims[0]; + if (p.kind !== 'polyline') throw new Error('erwartete polyline'); + expect(p.close).toBe(true); + }); + + it('chair: gefuelltes Rechteck mit Fallback-Farbe', () => { + const prims = elementToPrimitives(el('ch', 'chair', {}, { x: 50, y: 50, width: 20, height: 20 })); + const p = prims[0]; + if (p.kind !== 'rect') throw new Error('erwartete rect'); + expect(p.fill).toBe('#4a90d9'); + expect(p.x).toBe(40); + expect(p.y).toBe(40); + }); + + it('table: Kreis mit Fallback-Farbe und Radius aus width', () => { + const prims = elementToPrimitives(el('tb', 'table', {}, { x: 0, y: 0, width: 60, height: 60 })); + const p = prims[0]; + if (p.kind !== 'circle') throw new Error('erwartete circle'); + expect(p.radius).toBe(30); + expect(p.fill).toBe('#8b7355'); + }); + + it('stage: dunkles Rechteck', () => { + const prims = elementToPrimitives(el('st', 'stage', {}, { x: 0, y: 0, width: 200, height: 100 })); + const p = prims[0]; + if (p.kind !== 'rect') throw new Error('erwartete rect'); + expect(p.fill).toBe('#3a3a4a'); + expect(p.w).toBe(200); + expect(p.h).toBe(100); + }); });