From af9f53c61fdaf0bf4f8e202cf42ed927754cfa5d Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Wed, 16 Sep 2026 09:55:59 +0200 Subject: [PATCH] fix(CAD-13): double rendering - legacy canvas skips element rendering when pixi renderer is active (grid/background stay), defensive fallback if pixi fails --- frontend/src/canvas/RenderEngine.ts | 11 ++++ frontend/src/components/CanvasArea.tsx | 3 ++ frontend/tests/renderEngineSkip.test.ts | 72 +++++++++++++++++++++++++ 3 files changed, 86 insertions(+) create mode 100644 frontend/tests/renderEngineSkip.test.ts diff --git a/frontend/src/canvas/RenderEngine.ts b/frontend/src/canvas/RenderEngine.ts index 70d6d4c..1357318 100644 --- a/frontend/src/canvas/RenderEngine.ts +++ b/frontend/src/canvas/RenderEngine.ts @@ -55,6 +55,8 @@ export class RenderEngine { /** src des zuletzt erfolgreich geladenen Hintergrundbilds. */ private bgSrcLoaded = ''; private dpr = 1; + /** CAD-13-fix: true = Legacy-Canvas zeichnet KEINE Elemente (Pixi-Overlay zeichnet sie). Grid/Hintergrund bleiben. */ + skipElementRendering = false; constructor( canvas: HTMLCanvasElement, @@ -161,6 +163,15 @@ export class RenderEngine { if (this.options.showGrid) this.drawGrid(w, h); if (this.options.backgroundSrc) this.drawBackground(); this.drawRuler(w, h); + + // CAD-13-fix: Wenn Pixi-Renderer aktiv ist (skipElementRendering=true), + // zeichnet das Legacy-Canvas KEINE Elemente mehr (sonst doppelt) — + // nur Grid/Hintergrund/Lineal bleiben hier. + if (this.skipElementRendering) { + this.ctx.restore(); + return; + } + const viewport = this.zoomPan.getViewport(); const visibleElements = this.spatialIndex.search({ minX: viewport.minX, minY: viewport.minY, diff --git a/frontend/src/components/CanvasArea.tsx b/frontend/src/components/CanvasArea.tsx index 8fd1370..b7165b5 100644 --- a/frontend/src/components/CanvasArea.tsx +++ b/frontend/src/components/CanvasArea.tsx @@ -82,6 +82,9 @@ const CanvasArea: React.FC = ({ pixi = new PixiRenderer(); pixiRendererRef.current = pixi; pixi.init(pixiCanvas); + // CAD-13-fix: Pixi rendert jetzt die Elemente — Legacy-Canvas + // stellt nur noch Grid/Hintergrund/Lineal dar (sonst doppelt). + renderEngine.skipElementRendering = true; pixi.waitForReady().then(() => { // Task C4: Debug-Hook für Playwright-E2E-Assertions (window as unknown as { __v2Debug: unknown }).__v2Debug = { diff --git a/frontend/tests/renderEngineSkip.test.ts b/frontend/tests/renderEngineSkip.test.ts new file mode 100644 index 0000000..e5ead21 --- /dev/null +++ b/frontend/tests/renderEngineSkip.test.ts @@ -0,0 +1,72 @@ +/** + * Task CAD-13-fix: Legacy-Rendering doppelt (Legacy-Canvas + Pixi-Overlay + * zeichnen dieselben Elemente). skipElementRendering legt das Legacy- + * Element-Rendering still, wenn Pixi aktiv ist — Grid/Hintergrund/Lineal + * bleiben sichtbar. Defensiv: Ohne Flag ist Verhalten unveraendert. + */ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { RenderEngine } from '../src/canvas/RenderEngine'; +import { ZoomPanController } from '../src/canvas/ZoomPanController'; +import { SpatialIndex } from '../src/canvas/SpatialIndex'; +import { LayerManager } from '../src/canvas/LayerManager'; +import type { CADElement, CADLayer } from '../src/types/cad.types'; + +function setup() { + const canvas = document.createElement('canvas'); + canvas.width = 800; + canvas.height = 600; + document.body.appendChild(canvas); + const zpc = new ZoomPanController(canvas); + const spatialIndex = new SpatialIndex(); + const layerManager = new LayerManager(); + const engine = new RenderEngine(canvas, zpc, spatialIndex, layerManager); + return { canvas, engine, zpc, spatialIndex, layerManager }; +} + +describe('CAD-13-fix: RenderEngine.skipElementRendering', () => { + beforeEach(() => { + document.body.innerHTML = ''; + }); + + it('Default: Flag ist false (Verhalten unveraendert)', () => { + const { engine } = setup(); + expect(engine.skipElementRendering).toBe(false); + }); + + it('skipElementRendering=true: Elemente werden NICHT gezeichnet, Grid schon', () => { + const { engine, spatialIndex, layerManager } = setup(); + const layer: CADLayer = { id: 'l1', name: 'L1', visible: true, locked: false, color: '#fff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null } as CADLayer; + layerManager.addLayer(layer); + const el: CADElement = { + id: 'e1', type: 'line', layerId: 'l1', + x: 10, y: 10, width: 100, height: 100, + properties: { x1: 10, y1: 10, x2: 110, y2: 110 }, + } as unknown as CADElement; + spatialIndex.bulkInsert([el]); + engine.setLayers([layer]); + // drawElement darf nicht aufgerufen werden — wir spionieren die private + // Methode ueber einen Prototyp-Hook + const spy = vi.spyOn(engine as unknown as { drawElement: (el: CADElement, l: CADLayer) => void }, 'drawElement'); + engine.skipElementRendering = true; + engine.render(); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); + + it('skipElementRendering=false (Default): Elemente werden gezeichnet', () => { + const { engine, spatialIndex, layerManager } = setup(); + const layer: CADLayer = { id: 'l1', name: 'L1', visible: true, locked: false, color: '#fff', lineType: 'solid', transparency: 0, sortOrder: 0, parentId: null } as CADLayer; + layerManager.addLayer(layer); + const el: CADElement = { + id: 'e1', type: 'line', layerId: 'l1', + x: 10, y: 10, width: 100, height: 100, + properties: { x1: 10, y1: 10, x2: 110, y2: 110 }, + } as unknown as CADElement; + spatialIndex.bulkInsert([el]); + engine.setLayers([layer]); + const spy = vi.spyOn(engine as unknown as { drawElement: (el: CADElement, layer: CADLayer) => void }, 'drawElement'); + engine.render(); + expect(spy).toHaveBeenCalled(); + spy.mockRestore(); + }); +});