task(B4): cache background image (fixes BUG-4)
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user