From b4a855d6db24f7f986e240a8e68e59d6ff37bb51 Mon Sep 17 00:00:00 2001 From: Leopoldadmin Date: Mon, 22 Jun 2026 07:18:19 +0000 Subject: [PATCH] feat(canvas): Create RenderEngine with requestAnimationFrame loop, viewport culling, layer rendering, zoom/pan, grid and element rendering --- frontend/src/canvas/RenderEngine.ts | 240 ++++++++++++++++++++++++++++ 1 file changed, 240 insertions(+) create mode 100644 frontend/src/canvas/RenderEngine.ts diff --git a/frontend/src/canvas/RenderEngine.ts b/frontend/src/canvas/RenderEngine.ts new file mode 100644 index 0000000..0cc25ff --- /dev/null +++ b/frontend/src/canvas/RenderEngine.ts @@ -0,0 +1,240 @@ +import RBush from 'rbush'; +import { SpatialIndex } from './SpatialIndex'; +import { LayerManager } from './LayerManager'; +import { ZoomPanController } from './ZoomPanController'; + +type RenderElement = { + id: string; + type: 'line' | 'circle' | 'polyline' | 'polygon' | 'rect' | 'arc' | 'text' | 'dimension' | 'block_instance'; + layerId: string; + bbox: { minX: number; minY: number; maxX: number; maxY: number }; + // Additional properties based on type +}; + +type Layer = { + id: string; + name: string; + visible: boolean; + locked: boolean; +}; + +export class RenderEngine { + private canvas: HTMLCanvasElement; + private ctx: CanvasRenderingContext2D; + private spatialIndex: SpatialIndex; + private layerManager: LayerManager; + private zoomPanController: ZoomPanController; + private animationFrameId: number | null = null; + private elements: RenderElement[] = []; + private layers: Layer[] = []; + private isRendering = false; + + constructor(canvas: HTMLCanvasElement) { + this.canvas = canvas; + const ctx = canvas.getContext('2d'); + if (!ctx) { + throw new Error('Failed to get 2D context'); + } + this.ctx = ctx; + this.spatialIndex = new SpatialIndex(); + this.layerManager = new LayerManager(); + this.zoomPanController = new ZoomPanController(canvas); + } + + public start(): void { + if (this.isRendering) return; + this.isRendering = true; + this.render(); + } + + public stop(): void { + if (this.animationFrameId) { + cancelAnimationFrame(this.animationFrameId); + } + this.isRendering = false; + } + + private render(): void { + if (!this.isRendering) return; + + // Clear canvas + this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + + // Apply zoom/pan transforms + const transform = this.zoomPanController.getTransform(); + this.ctx.save(); + this.ctx.transform(transform.a, transform.b, transform.c, transform.d, transform.e, transform.f); + + // Render grid + this.renderGrid(); + + // Get visible elements using viewport culling + const viewport = this.zoomPanController.getViewport(); + const visibleElements = this.spatialIndex.search(viewport); + + // Render elements by layer order + const visibleLayers = this.layerManager.getVisibleLayers(); + for (const layer of visibleLayers) { + const layerElements = visibleElements.filter(el => el.layerId === layer.id); + for (const element of layerElements) { + this.renderElement(element); + } + } + + this.ctx.restore(); + + // Continue render loop + this.animationFrameId = requestAnimationFrame(() => this.render()); + } + + private renderGrid(): void { + const gridSize = 50; // pixels + const viewport = this.zoomPanController.getViewport(); + + this.ctx.strokeStyle = '#e0e0e0'; + this.ctx.lineWidth = 1; + + // Vertical lines + for (let x = Math.floor(viewport.minX / gridSize) * gridSize; x < viewport.maxX; x += gridSize) { + this.ctx.beginPath(); + this.ctx.moveTo(x, viewport.minY); + this.ctx.lineTo(x, viewport.maxY); + this.ctx.stroke(); + } + + // Horizontal lines + for (let y = Math.floor(viewport.minY / gridSize) * gridSize; y < viewport.maxY; y += gridSize) { + this.ctx.beginPath(); + this.ctx.moveTo(viewport.minX, y); + this.ctx.lineTo(viewport.maxX, y); + this.ctx.stroke(); + } + } + + private renderElement(element: RenderElement): void { + switch (element.type) { + case 'line': + this.renderLine(element); + break; + case 'circle': + this.renderCircle(element); + break; + case 'polyline': + this.renderPolyline(element); + break; + case 'polygon': + this.renderPolygon(element); + break; + case 'rect': + this.renderRect(element); + break; + case 'arc': + this.renderArc(element); + break; + case 'text': + this.renderText(element); + break; + case 'dimension': + this.renderDimension(element); + break; + case 'block_instance': + this.renderBlockInstance(element); + break; + } + } + + private renderLine(element: RenderElement): void { + // Implementation for line rendering + this.ctx.beginPath(); + this.ctx.moveTo(0, 0); // Placeholder + this.ctx.lineTo(100, 100); // Placeholder + this.ctx.stroke(); + } + + private renderCircle(element: RenderElement): void { + // Implementation for circle rendering + this.ctx.beginPath(); + this.ctx.arc(50, 50, 30, 0, Math.PI * 2); // Placeholder + this.ctx.stroke(); + } + + private renderPolyline(element: RenderElement): void { + // Implementation for polyline rendering + this.ctx.beginPath(); + this.ctx.moveTo(0, 0); // Placeholder + this.ctx.lineTo(50, 50); // Placeholder + this.ctx.lineTo(100, 0); // Placeholder + this.ctx.stroke(); + } + + private renderPolygon(element: RenderElement): void { + // Implementation for polygon rendering + this.ctx.beginPath(); + this.ctx.moveTo(0, 0); // Placeholder + this.ctx.lineTo(50, 50); // Placeholder + this.ctx.lineTo(100, 0); // Placeholder + this.ctx.closePath(); + this.ctx.stroke(); + } + + private renderRect(element: RenderElement): void { + // Implementation for rectangle rendering + this.ctx.strokeRect(10, 10, 100, 50); // Placeholder + } + + private renderArc(element: RenderElement): void { + // Implementation for arc rendering + this.ctx.beginPath(); + this.ctx.arc(50, 50, 30, 0, Math.PI); // Placeholder + this.ctx.stroke(); + } + + private renderText(element: RenderElement): void { + // Implementation for text rendering + this.ctx.fillText('Sample Text', 20, 20); // Placeholder + } + + private renderDimension(element: RenderElement): void { + // Implementation for dimension rendering + this.ctx.beginPath(); + this.ctx.moveTo(0, 0); // Placeholder + this.ctx.lineTo(100, 0); // Placeholder + this.ctx.stroke(); + + // Draw dimension lines and text + this.ctx.fillText('100', 50, -5); // Placeholder + } + + private renderBlockInstance(element: RenderElement): void { + // Implementation for block instance rendering + this.ctx.strokeRect(20, 20, 60, 40); // Placeholder + this.ctx.fillText('BLOCK', 30, 45); // Placeholder + } + + public addElement(element: RenderElement): void { + this.elements.push(element); + this.spatialIndex.insert(element); + } + + public removeElement(id: string): void { + const index = this.elements.findIndex(el => el.id === id); + if (index !== -1) { + const element = this.elements[index]; + this.elements.splice(index, 1); + this.spatialIndex.remove(element); + } + } + + public addLayer(layer: Layer): void { + this.layers.push(layer); + this.layerManager.addLayer(layer); + } + + public updateLayer(layer: Layer): void { + const index = this.layers.findIndex(l => l.id === layer.id); + if (index !== -1) { + this.layers[index] = layer; + this.layerManager.updateLayer(layer); + } + } +}