task(B4): cache background image (fixes BUG-4)

This commit is contained in:
Agent Zero
2026-08-26 20:51:43 +02:00
parent d8f29e42e3
commit d93ee5308c
2 changed files with 98 additions and 6 deletions
+25 -4
View File
@@ -50,6 +50,10 @@ export class RenderEngine {
private snapPoints: SnapPoint[] = [];
private activeSnapPoint: SnapPoint | null = null;
private previewElement: CADElement | null = null;
/** Gecachtes Hintergrundbild (BUG-4-Fix): nur 1× pro backgroundSrc laden. */
private bgImage: HTMLImageElement | null = null;
/** src des zuletzt erfolgreich geladenen Hintergrundbilds. */
private bgSrcLoaded = '';
private dpr = 1;
constructor(
@@ -379,13 +383,30 @@ export class RenderEngine {
}
private drawBackground(): void {
// Background image rendering with transform
// Background image rendering with transform.
// BUG-4-Fix: Das Bild wird nur EINMAL pro backgroundSrc geladen und gecacht;
// vorher wurde pro Frame ein neues Image erzeugt (Flackern/Rerender-Schleife).
const src = this.options.backgroundSrc!;
// Quellwechsel: Cache invalidieren und neu laden
if (!this.bgImage || this.bgSrcLoaded !== src) {
const img = new Image();
img.src = this.options.backgroundSrc!;
if (!img.complete) {
img.onload = () => this.render();
img.onload = () => {
this.bgImage = img;
this.bgSrcLoaded = src;
this.render(); // einmalig nach erfolgreichem Laden neu zeichnen
};
img.src = src;
if (img.complete && img.naturalWidth > 0) {
// Bereits aus Browser-Cache verfügbar (z.B. dataURL)
this.bgImage = img;
this.bgSrcLoaded = src;
}
return;
}
// Gecachtes Bild zeichnen
const img = this.bgImage;
this.ctx.save();
this.ctx.globalAlpha = this.options.backgroundOpacity;
const s = this.zoomPan.getScale();
+72 -1
View File
@@ -3,10 +3,14 @@
* Diese Tests sind VOR dem jeweiligen Fix ROT (dokumentiertes Fehlverhalten)
* und müssen danach GRÜN sein.
*/
import { describe, it, expect } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { SeatingService } from '../src/services/seatingService';
import { trimElement, offsetElement } from '../src/tools/modification/geometry';
import type { CADElement } from '../src/types/cad.types';
import { RenderEngine } from '../src/canvas/RenderEngine';
import { ZoomPanController } from '../src/canvas/ZoomPanController';
import { SpatialIndex } from '../src/canvas/SpatialIndex';
import { LayerManager } from '../src/canvas/LayerManager';
// ── Helfer (Muster aus tests/geometry.test.ts) ─────────────
function makeLine(id: string, x1: number, y1: number, x2: number, y2: number): CADElement {
@@ -100,3 +104,70 @@ describe('BUG-3: offset respects click side', () => {
expect(offsetElement(circle, 10, 1).properties.radius).toBeCloseTo(60);
});
});
// ── BUG-4: Hintergrundbild wurde pro Frame neu geladen ──────
describe('BUG-4: background image cached', () => {
let imageCount: number;
let OriginalImage: typeof Image;
beforeEach(() => {
imageCount = 0;
OriginalImage = global.Image;
// Bild-Stub: zaehlt Konstruktur-Aufrufe; simuliert Browser-Cache-Bild
// (complete=true sofort verfuegbar, onload als Mikrotask wie async decode)
class FakeImage {
complete = true;
naturalWidth = 800;
width = 800;
height = 600;
onload: (() => void) | null = null;
private internalSrc = '';
get src(): string {
return this.internalSrc;
}
set src(value: string) {
this.internalSrc = value;
queueMicrotask(() => this.onload?.());
}
constructor() {
imageCount += 1;
}
}
(global as { Image: unknown }).Image = FakeImage as unknown as typeof Image;
});
afterEach(() => {
(global as { Image: unknown }).Image = OriginalImage;
});
function createEngine(): RenderEngine {
const canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
const zoomPan = new ZoomPanController(canvas);
const spatialIndex = new SpatialIndex();
const layerManager = new LayerManager();
return new RenderEngine(canvas, zoomPan, spatialIndex, layerManager);
}
it('lädt das Bild pro Quellwechsel nur EINMAL, weitere Frames nutzen den Cache', async () => {
const engine = createEngine();
const DATA_URL = 'data:image/png;base64,iVBORw0KGgo=';
engine.setOptions({ backgroundSrc: DATA_URL, backgroundOpacity: 0.5 });
// Frame 1: legt genau ein Image an und cachet es (complete=true)
engine.render();
expect(imageCount).toBe(1);
// Weitere Frames mit identischer src: dürfen KEIN neues Image erzeugen
engine.render();
engine.render();
engine.render();
expect(imageCount).toBe(1);
// Ausstehende Mikrotasks (simuliertes onload) abwarten, dann erneut prüfen:
await Promise.resolve();
engine.render();
expect(imageCount).toBe(1); // vorher (Bug): jedes render() erzeugte ein neues Image
});
});