( gpuContext: GPUContextLike, options: ResolvedChartGPUOptions, callbacks?: RenderCoordinatorCallbacks )
| 1467 | }; |
| 1468 | |
| 1469 | export function createRenderCoordinator( |
| 1470 | gpuContext: GPUContextLike, |
| 1471 | options: ResolvedChartGPUOptions, |
| 1472 | callbacks?: RenderCoordinatorCallbacks |
| 1473 | ): RenderCoordinator { |
| 1474 | if (!gpuContext.initialized) { |
| 1475 | throw new Error('RenderCoordinator: gpuContext must be initialized.'); |
| 1476 | } |
| 1477 | const device = gpuContext.device; |
| 1478 | if (!device) { |
| 1479 | throw new Error('RenderCoordinator: gpuContext.device is required.'); |
| 1480 | } |
| 1481 | if (!gpuContext.canvas) { |
| 1482 | throw new Error('RenderCoordinator: gpuContext.canvas is required.'); |
| 1483 | } |
| 1484 | if (!gpuContext.canvasContext) { |
| 1485 | throw new Error('RenderCoordinator: gpuContext.canvasContext is required.'); |
| 1486 | } |
| 1487 | |
| 1488 | // Listen for device loss and emit callback |
| 1489 | // Note: We don't call dispose() here to avoid double-cleanup if user calls dispose() in callback. |
| 1490 | // The coordinator is effectively non-functional after device loss until re-created. |
| 1491 | device.lost.then((info) => { |
| 1492 | callbacks?.onDeviceLost?.(info.message || info.reason || 'unknown'); |
| 1493 | }).catch(() => { |
| 1494 | // Ignore errors in device.lost promise (can occur if device is destroyed before lost promise resolves) |
| 1495 | }); |
| 1496 | |
| 1497 | const targetFormat = gpuContext.preferredFormat ?? DEFAULT_TARGET_FORMAT; |
| 1498 | |
| 1499 | // DOM-dependent features (overlays, legends) require HTMLCanvasElement and domOverlays !== false. |
| 1500 | // OffscreenCanvas is for rendering only. |
| 1501 | const domOverlaysEnabled = callbacks?.domOverlays !== false; |
| 1502 | const overlayContainer = domOverlaysEnabled && isHTMLCanvasElement(gpuContext.canvas) ? gpuContext.canvas.parentElement : null; |
| 1503 | const axisLabelOverlay: TextOverlay | null = overlayContainer ? createTextOverlay(overlayContainer) : null; |
| 1504 | // Dedicated overlay for annotations (do not reuse axis label overlay). |
| 1505 | const annotationOverlay: TextOverlay | null = overlayContainer ? createTextOverlay(overlayContainer) : null; |
| 1506 | const legend: Legend | null = overlayContainer ? createLegend(overlayContainer, 'right') : null; |
| 1507 | // Text measurement for axis labels. Only available in DOM contexts (not worker threads). |
| 1508 | const tickMeasureCtx: CanvasRenderingContext2D | null = (() => { |
| 1509 | if (typeof document === 'undefined') { |
| 1510 | // Worker thread: DOM not available. |
| 1511 | return null; |
| 1512 | } |
| 1513 | try { |
| 1514 | const c = document.createElement('canvas'); |
| 1515 | return c.getContext('2d'); |
| 1516 | } catch { |
| 1517 | return null; |
| 1518 | } |
| 1519 | })(); |
| 1520 | const tickMeasureCache: Map<string, number> | null = tickMeasureCtx ? new Map() : null; |
| 1521 | |
| 1522 | let disposed = false; |
| 1523 | let currentOptions: ResolvedChartGPUOptions = options; |
| 1524 | let lastSeriesCount = options.series.length; |
| 1525 | |
| 1526 | // Story 5.16: initial-load intro animation (series marks only). |
no test coverage detected