|
|
|
|
|
|
|
|
|
|
|
import { VECTOR_EXPORT_CONFIG } from '@/config/vectorExportConfig'; |
|
import { svg2Base64 } from './svg2Base64'; |
|
import { renderElementToBase64, getElementDimensions } from './canvasRenderer'; |
|
|
|
|
|
export enum RenderStrategy { |
|
SVG_SERIALIZATION = 'svg_serialization', |
|
CANVAS_RENDER = 'canvas_render', |
|
SIMPLIFIED_SVG = 'simplified_svg', |
|
PLACEHOLDER = 'placeholder' |
|
} |
|
|
|
|
|
export interface RenderResult { |
|
success: boolean; |
|
data?: string; |
|
strategy: RenderStrategy; |
|
duration: number; |
|
error?: Error; |
|
metadata?: { |
|
width: number; |
|
height: number; |
|
format: string; |
|
size: number; |
|
}; |
|
} |
|
|
|
|
|
class PerformanceMonitor { |
|
private static instance: PerformanceMonitor; |
|
private metrics: Map<string, number[]> = new Map(); |
|
|
|
static getInstance(): PerformanceMonitor { |
|
if (!this.instance) { |
|
this.instance = new PerformanceMonitor(); |
|
} |
|
return this.instance; |
|
} |
|
|
|
recordMetric(operation: string, duration: number): void { |
|
if (!VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.MONITORING.ENABLED) return; |
|
|
|
if (!this.metrics.has(operation)) { |
|
this.metrics.set(operation, []); |
|
} |
|
|
|
const metrics = this.metrics.get(operation)!; |
|
metrics.push(duration); |
|
|
|
|
|
const maxCount = VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.MONITORING.MAX_METRICS_COUNT; |
|
if (metrics.length > maxCount) { |
|
metrics.shift(); |
|
} |
|
} |
|
|
|
getAverageTime(operation: string): number { |
|
const metrics = this.metrics.get(operation); |
|
if (!metrics || metrics.length === 0) return 0; |
|
return metrics.reduce((sum, time) => sum + time, 0) / metrics.length; |
|
} |
|
|
|
getMetrics(): Record<string, { average: number; count: number; latest: number }> { |
|
const result: Record<string, { average: number; count: number; latest: number }> = {}; |
|
|
|
for (const [operation, times] of this.metrics.entries()) { |
|
if (times.length > 0) { |
|
result[operation] = { |
|
average: this.getAverageTime(operation), |
|
count: times.length, |
|
latest: times[times.length - 1] |
|
}; |
|
} |
|
} |
|
|
|
return result; |
|
} |
|
} |
|
|
|
|
|
class ErrorHandler { |
|
static logError(error: Error, context: string, element?: Element): void { |
|
const config = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.LOGGING; |
|
|
|
if (config.ERROR_ONLY && VECTOR_EXPORT_CONFIG.ENVIRONMENT.isProduction()) { |
|
console.error(`VectorRenderManager[${context}]: ${error.message}`); |
|
} else if (config.VERBOSE) { |
|
console.error(`VectorRenderManager[${context}]:`, { |
|
error: error.message, |
|
stack: config.INCLUDE_STACK_TRACE ? error.stack : undefined, |
|
element: element ? { |
|
tagName: element.tagName, |
|
className: element.className, |
|
id: element.id |
|
} : undefined |
|
}); |
|
} |
|
} |
|
|
|
static async retry<T>( |
|
operation: () => Promise<T>, |
|
context: string, |
|
maxAttempts: number = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.RETRY.MAX_ATTEMPTS |
|
): Promise<T> { |
|
let lastError: Error; |
|
|
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) { |
|
try { |
|
return await operation(); |
|
} catch (error) { |
|
lastError = error as Error; |
|
|
|
if (attempt === maxAttempts) { |
|
this.logError(lastError, `${context} (final attempt ${attempt}/${maxAttempts})`); |
|
throw lastError; |
|
} |
|
|
|
const delay = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.RETRY.DELAY_MS * |
|
Math.pow(VECTOR_EXPORT_CONFIG.ERROR_CONFIG.RETRY.BACKOFF_MULTIPLIER, attempt - 1); |
|
|
|
this.logError(lastError, `${context} (attempt ${attempt}/${maxAttempts}, retrying in ${delay}ms)`); |
|
await new Promise(resolve => setTimeout(resolve, delay)); |
|
} |
|
} |
|
|
|
throw lastError!; |
|
} |
|
} |
|
|
|
|
|
export class VectorRenderManager { |
|
private static instance: VectorRenderManager; |
|
private monitor: PerformanceMonitor; |
|
|
|
private constructor() { |
|
this.monitor = PerformanceMonitor.getInstance(); |
|
} |
|
|
|
static getInstance(): VectorRenderManager { |
|
if (!this.instance) { |
|
this.instance = new VectorRenderManager(); |
|
} |
|
return this.instance; |
|
} |
|
|
|
|
|
|
|
|
|
async renderElement(element: Element, preferredStrategy?: RenderStrategy): Promise<RenderResult> { |
|
const startTime = performance.now(); |
|
|
|
|
|
const validation = this.validateElement(element); |
|
if (!validation.valid) { |
|
return { |
|
success: false, |
|
strategy: RenderStrategy.PLACEHOLDER, |
|
duration: performance.now() - startTime, |
|
error: new Error(validation.reason || 'Element validation failed') |
|
}; |
|
} |
|
|
|
|
|
const strategies = this.getStrategies(element, preferredStrategy); |
|
|
|
|
|
for (const strategy of strategies) { |
|
try { |
|
const result = await this.executeStrategy(element, strategy); |
|
|
|
if (result.success) { |
|
const duration = performance.now() - startTime; |
|
this.monitor.recordMetric(`render_success_${strategy}`, duration); |
|
|
|
return { |
|
...result, |
|
duration |
|
}; |
|
} |
|
} catch (error) { |
|
ErrorHandler.logError(error as Error, `Strategy ${strategy}`, element); |
|
continue; |
|
} |
|
} |
|
|
|
|
|
const duration = performance.now() - startTime; |
|
this.monitor.recordMetric('render_fallback_placeholder', duration); |
|
|
|
return this.createPlaceholder(element, duration); |
|
} |
|
|
|
|
|
|
|
|
|
private validateElement(element: Element): { valid: boolean; reason?: string } { |
|
if (!element) { |
|
return { valid: false, reason: 'Element is null or undefined' }; |
|
} |
|
|
|
|
|
const dimensions = getElementDimensions(element as HTMLElement); |
|
const config = VECTOR_EXPORT_CONFIG.VALIDATION_CONFIG.DIMENSIONS; |
|
|
|
if (!dimensions.isValid) { |
|
return { valid: false, reason: 'Element has invalid dimensions' }; |
|
} |
|
|
|
if (dimensions.width < config.MIN_WIDTH || dimensions.height < config.MIN_HEIGHT) { |
|
return { valid: false, reason: 'Element dimensions too small' }; |
|
} |
|
|
|
if (dimensions.width > config.MAX_WIDTH || dimensions.height > config.MAX_HEIGHT) { |
|
return { valid: false, reason: 'Element dimensions too large' }; |
|
} |
|
|
|
return { valid: true }; |
|
} |
|
|
|
|
|
|
|
|
|
private getStrategies(element: Element, preferred?: RenderStrategy): RenderStrategy[] { |
|
const isSVG = element.tagName.toLowerCase() === 'svg'; |
|
const isHuggingface = VECTOR_EXPORT_CONFIG.ENVIRONMENT.isHuggingface(); |
|
const canvasSupported = VECTOR_EXPORT_CONFIG.COMPATIBILITY_CONFIG.FEATURES.CANVAS_SUPPORT(); |
|
|
|
let strategies: RenderStrategy[] = []; |
|
|
|
|
|
if (preferred) { |
|
strategies.push(preferred); |
|
} |
|
|
|
|
|
if (isSVG) { |
|
if (!strategies.includes(RenderStrategy.SVG_SERIALIZATION)) { |
|
strategies.push(RenderStrategy.SVG_SERIALIZATION); |
|
} |
|
if (!strategies.includes(RenderStrategy.SIMPLIFIED_SVG)) { |
|
strategies.push(RenderStrategy.SIMPLIFIED_SVG); |
|
} |
|
} |
|
|
|
|
|
if (canvasSupported && !isHuggingface && !strategies.includes(RenderStrategy.CANVAS_RENDER)) { |
|
strategies.push(RenderStrategy.CANVAS_RENDER); |
|
} |
|
|
|
|
|
if (!strategies.includes(RenderStrategy.PLACEHOLDER)) { |
|
strategies.push(RenderStrategy.PLACEHOLDER); |
|
} |
|
|
|
return strategies; |
|
} |
|
|
|
|
|
|
|
|
|
private async executeStrategy(element: Element, strategy: RenderStrategy): Promise<RenderResult> { |
|
const startTime = performance.now(); |
|
|
|
switch (strategy) { |
|
case RenderStrategy.SVG_SERIALIZATION: |
|
return this.executeSVGSerialization(element, startTime); |
|
|
|
case RenderStrategy.CANVAS_RENDER: |
|
return this.executeCanvasRender(element, startTime); |
|
|
|
case RenderStrategy.SIMPLIFIED_SVG: |
|
return this.executeSimplifiedSVG(element, startTime); |
|
|
|
case RenderStrategy.PLACEHOLDER: |
|
return this.createPlaceholder(element, performance.now() - startTime); |
|
|
|
default: |
|
throw new Error(`Unknown strategy: ${strategy}`); |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
private async executeSVGSerialization(element: Element, startTime: number): Promise<RenderResult> { |
|
try { |
|
const base64 = svg2Base64(element); |
|
const duration = performance.now() - startTime; |
|
|
|
return { |
|
success: true, |
|
data: base64, |
|
strategy: RenderStrategy.SVG_SERIALIZATION, |
|
duration, |
|
metadata: { |
|
width: element.getBoundingClientRect().width, |
|
height: element.getBoundingClientRect().height, |
|
format: 'svg', |
|
size: base64.length |
|
} |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
strategy: RenderStrategy.SVG_SERIALIZATION, |
|
duration: performance.now() - startTime, |
|
error: error as Error |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
private async executeCanvasRender(element: Element, startTime: number): Promise<RenderResult> { |
|
try { |
|
const base64 = await renderElementToBase64(element as HTMLElement, { |
|
scale: VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.CANVAS.DEFAULT_SCALE, |
|
backgroundColor: VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.CANVAS.BACKGROUND_COLOR, |
|
timeout: VECTOR_EXPORT_CONFIG.PERFORMANCE_CONFIG.RENDER_TIMEOUT |
|
}); |
|
|
|
const duration = performance.now() - startTime; |
|
|
|
return { |
|
success: true, |
|
data: base64, |
|
strategy: RenderStrategy.CANVAS_RENDER, |
|
duration, |
|
metadata: { |
|
width: element.getBoundingClientRect().width, |
|
height: element.getBoundingClientRect().height, |
|
format: 'png', |
|
size: base64.length |
|
} |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
strategy: RenderStrategy.CANVAS_RENDER, |
|
duration: performance.now() - startTime, |
|
error: error as Error |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
private async executeSimplifiedSVG(element: Element, startTime: number): Promise<RenderResult> { |
|
try { |
|
const rect = element.getBoundingClientRect(); |
|
const width = rect.width || 100; |
|
const height = rect.height || 100; |
|
|
|
|
|
const simplifiedSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> |
|
<rect width="100%" height="100%" fill="#f0f0f0" stroke="#ccc" stroke-width="1"/> |
|
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="14" fill="#666">SVG</text> |
|
</svg>`; |
|
|
|
const base64 = `data:image/svg+xml;base64,${btoa(simplifiedSvg)}`; |
|
const duration = performance.now() - startTime; |
|
|
|
return { |
|
success: true, |
|
data: base64, |
|
strategy: RenderStrategy.SIMPLIFIED_SVG, |
|
duration, |
|
metadata: { |
|
width, |
|
height, |
|
format: 'svg', |
|
size: base64.length |
|
} |
|
}; |
|
} catch (error) { |
|
return { |
|
success: false, |
|
strategy: RenderStrategy.SIMPLIFIED_SVG, |
|
duration: performance.now() - startTime, |
|
error: error as Error |
|
}; |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
private createPlaceholder(element: Element, duration: number): RenderResult { |
|
const rect = element.getBoundingClientRect(); |
|
const width = rect.width || 100; |
|
const height = rect.height || 100; |
|
const config = VECTOR_EXPORT_CONFIG.ERROR_CONFIG.FALLBACK; |
|
|
|
const placeholderSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"> |
|
<rect width="100%" height="100%" fill="${config.PLACEHOLDER_COLOR}" stroke="#ddd" stroke-width="1"/> |
|
<text x="50%" y="50%" text-anchor="middle" dy="0.3em" font-size="12" fill="#999">${config.PLACEHOLDER_TEXT}</text> |
|
</svg>`; |
|
|
|
const base64 = `data:image/svg+xml;base64,${btoa(placeholderSvg)}`; |
|
|
|
return { |
|
success: true, |
|
data: base64, |
|
strategy: RenderStrategy.PLACEHOLDER, |
|
duration, |
|
metadata: { |
|
width, |
|
height, |
|
format: 'svg', |
|
size: base64.length |
|
} |
|
}; |
|
} |
|
|
|
|
|
|
|
|
|
getPerformanceMetrics(): Record<string, { average: number; count: number; latest: number }> { |
|
return this.monitor.getMetrics(); |
|
} |
|
} |
|
|
|
|
|
export const vectorRenderManager = VectorRenderManager.getInstance(); |
|
export default vectorRenderManager; |