feat(canvas): Create RenderEngine with requestAnimationFrame loop, viewport culling, layer rendering, zoom/pan, grid and element rendering
This commit is contained in:
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user