task(C1): render adapter interface with headless mock adapter

This commit is contained in:
Agent Zero
2026-08-27 08:43:19 +02:00
parent ebe8cd32d4
commit c962e755ec
3 changed files with 312 additions and 0 deletions
+115
View File
@@ -0,0 +1,115 @@
/**
* MockAdapter Headless-Implementierung des RenderAdapter (Task C1).
* Zeichnet nichts; protokolliert alle Aufrufe für assertions in Unit-Tests.
*/
import type {
RenderAdapter,
Viewport,
GridConfig,
BackgroundDrawConfig,
RulerConfig,
LayerStyle,
OverlayPainter,
SelectionState,
} from '../types';
import type { CADElement, CADLayer } from '../../types/cad.types';
import type { Pt } from '../../tools/modification/geometry';
export interface RecordedCall {
method: string;
args: unknown[];
}
export class MockAdapter implements RenderAdapter {
readonly calls: RecordedCall[] = [];
private canvas: HTMLCanvasElement | null = null;
nextHitTestResult: CADElement | null = null;
// ── Recording helper ─────────────────────────────────────
private rec(method: string, args: unknown[]): void {
this.calls.push({ method, args });
}
/** Test-Helfer: alle Aufrufe einer Methode. */
callsOf(method: string): RecordedCall[] {
return this.calls.filter((c) => c.method === method);
}
/** Test-Helfer: Anzahl Aufrufe einer Methode. */
countCalls(method: string): number {
return this.callsOf(method).length;
}
reset(): void {
this.calls.length = 0;
this.nextHitTestResult = null;
}
// ── RenderAdapter Implementierung (reines Recording) ─────
init(canvas: HTMLCanvasElement): void {
this.canvas = canvas;
this.rec('init', [canvas]);
}
resize(width: number, height: number, dpr: number): void {
this.rec('resize', [width, height, dpr]);
}
setViewport(vp: Viewport): void {
this.rec('setViewport', [vp]);
}
destroy(): void {
this.rec('destroy', []);
}
clear(color: string): void {
this.rec('clear', [color]);
}
drawGrid(cfg: GridConfig): void {
this.rec('drawGrid', [cfg]);
}
drawBackground(img: HTMLImageElement | null, cfg: BackgroundDrawConfig): void {
this.rec('drawBackground', [img, cfg]);
}
drawRuler(cfg: RulerConfig): void {
this.rec('drawRuler', [cfg]);
}
drawElement(el: CADElement, layer: CADLayer, style: LayerStyle): void {
this.rec('drawElement', [el, layer, style]);
}
drawPreview(el: CADElement | null): void {
this.rec('drawPreview', [el]);
}
setSelection(sel: SelectionState): void {
this.rec('setSelection', [sel]);
}
setSnapPoints(pts: Array<{ x: number; y: number; type: string }>): void {
this.rec('setSnapPoints', [pts]);
}
overlay(draw: (g: OverlayPainter) => void): void {
const painter: OverlayPainter = {
line(a: Pt, b: Pt, color: string, width?: number) {},
rect(x: number, y: number, w: number, h: number, strokeColor: string) {},
circle(center: Pt, radius: number, strokeColor: string) {},
text(text: string, at: Pt, color: string, fontSize?: number) {},
};
draw(painter);
this.rec('overlay', [null]);
}
hitTest(world: Pt, tolerancePx: number): CADElement | null {
this.rec('hitTest', [world, tolerancePx]);
return this.nextHitTestResult;
}
}
+115
View File
@@ -0,0 +1,115 @@
/**
* render/types.ts RenderAdapter-Schnittstelle (Task C1).
*
* Ziel-Architektur: ALLES Rendering läuft gegen diese schmale API.
* Implementierungen:
* - render/pixi/PixiRenderer.ts (WebGL, Task C2+)
* - tests/helpers/mockAdapter.ts (Headless-Recorder für Unit-Tests)
*
* Phase D/E-Erweiterungen (Plugins, Custom-Elemente) nutzen denselben Vertrag
* über drawOverlay/drawCustom.
*/
import type { CADElement, CADLayer } from '../types/cad.types';
import type { Pt } from '../tools/modification/geometry';
/** Aktueller Ausschnitt in Weltkoordinaten + Skalierung. */
export interface Viewport {
x: number;
y: number;
scale: number;
width: number;
height: number;
}
/** Zeichenraster-Konfiguration. */
export interface GridConfig {
enabled: boolean;
size: number;
color: string;
}
/** Hintergrundbild-Konfiguration (Spiegelung der Legacy RenderOptions-Felder). */
export interface BackgroundDrawConfig {
src?: string;
opacity?: number;
offsetX?: number;
offsetY?: number;
scale?: number;
rotation?: number;
}
/** Lineal-Konfiguration. */
export interface RulerConfig {
enabled: boolean;
unit: string;
scaleFactor: number;
}
/** Stil pro CAD-Layer (Farbe/Linientyp/Transparenz). */
export interface LayerStyle {
color: string;
lineType: string;
transparency: number;
visible: boolean;
}
/** Freihand-Zeichner für temporäre Overlays (Marquee, Tool-Hilfslinien). */
export interface OverlayPainter {
line(a: Pt, b: Pt, color: string, width?: number): void;
rect(x: number, y: number, w: number, h: number, strokeColor: string): void;
circle(center: Pt, radius: number, strokeColor: string): void;
text(text: string, at: Pt, color: string, fontSize?: number): void;
}
/** Selektionszustand für die Anzeige von Handles. */
export interface SelectionState {
selectedIds: string[];
}
/**
* Der zentrale Rendervertrag. Implementierungen MÜSSEN alle Methoden bereitstellen,
* auch wenn einzelne initial No-Ops sind (z.B. drawBackground ohne Bild).
*/
export interface RenderAdapter {
/** Bindet den Renderer an ein Canvas-Element (einmalig). */
init(canvas: HTMLCanvasElement): void;
/** Reaktion auf Größenänderungen (DPR-bewusst). */
resize(width: number, height: number, dpr: number): void;
/** Setzt den aktuellen Ausschnitt; danach ist ein neuer Frame nötig. */
setViewport(vp: Viewport): void;
/// Gibt WebGL-/DOM-Ressourcen frei. */
destroy(): void;
/** Leert die Zeichenfläche mit einer Grundfarbe. */
clear(color: string): void;
/** Zeichnet das Raster. */
drawGrid(cfg: GridConfig): void;
/** Zeichnet das Hintergrundbild, falls konfiguriert. */
drawBackground(img: HTMLImageElement | null, cfg: BackgroundDrawConfig): void;
/** Zeichnet das Lineal. */
drawRuler(cfg: RulerConfig): void;
/** Zeichnet ein Element im Kontext seines Layers (Stil, Sichtbarkeit). */
drawElement(el: CADElement, layer: CADLayer, style: LayerStyle): void;
/** Zeichnet ein Element in Transformation / Live-Vorschau. */
drawPreview(el: CADElement | null): void;
/** Synchronisiert die Selektionsanzeige (Handles, Umrandungen). */
setSelection(sel: SelectionState): void;
/** Snap-Markierungen setzen/entfernen. */
setSnapPoints(pts: Array<{ x: number; y: number; type: string }>): void;
/** Öffnet einen Overlay-Pass für freihand-Overlays (Tools). */
overlay(draw: (g: OverlayPainter) => void): void;
/** Treffer abfragen (Delegation an SpatialIndex; kein GPU-Picking). */
hitTest(world: Pt, tolerancePx: number): CADElement | null;
}