(device: GPUDevice)
| 89 | } |
| 90 | |
| 91 | export function createDataStore(device: GPUDevice): DataStore { |
| 92 | const series = new Map<number, SeriesEntry>(); |
| 93 | let disposed = false; |
| 94 | |
| 95 | // Type guard (avoid relying on Array.isArray narrowing for readonly tuples in strict TS configs). |
| 96 | const isTupleDataPoint = (p: DataPoint): p is DataPointTuple => Array.isArray(p); |
| 97 | |
| 98 | const packDataPointsWithXOffset = (points: ReadonlyArray<DataPoint>, xOffset: number): Float32Array => { |
| 99 | if (!points || points.length === 0) return new Float32Array(0); |
| 100 | |
| 101 | const buffer = new ArrayBuffer(points.length * 2 * 4); |
| 102 | const f32 = new Float32Array(buffer); |
| 103 | |
| 104 | // Hot path: keep logic minimal (validation happens elsewhere in option resolution). |
| 105 | for (let i = 0; i < points.length; i++) { |
| 106 | const p = points[i]!; |
| 107 | const x = isTupleDataPoint(p) ? p[0] : p.x; |
| 108 | const y = isTupleDataPoint(p) ? p[1] : p.y; |
| 109 | |
| 110 | // Subtracting before the Float32 cast preserves sub-ULP deltas for large x magnitudes. |
| 111 | f32[i * 2 + 0] = x - xOffset; |
| 112 | f32[i * 2 + 1] = y; |
| 113 | } |
| 114 | |
| 115 | return f32; |
| 116 | }; |
| 117 | |
| 118 | const assertNotDisposed = (): void => { |
| 119 | if (disposed) { |
| 120 | throw new Error('DataStore is disposed.'); |
| 121 | } |
| 122 | }; |
| 123 | |
| 124 | const getSeriesEntry = (index: number): SeriesEntry => { |
| 125 | assertNotDisposed(); |
| 126 | const entry = series.get(index); |
| 127 | if (!entry) { |
| 128 | throw new Error(`Series ${index} has no data. Call setSeries(${index}, data) first.`); |
| 129 | } |
| 130 | return entry; |
| 131 | }; |
| 132 | |
| 133 | const setSeries = (index: number, data: ReadonlyArray<DataPoint>, options?: Readonly<{ xOffset?: number }>): void => { |
| 134 | assertNotDisposed(); |
| 135 | |
| 136 | const xOffset = options?.xOffset ?? 0; |
| 137 | const packed = xOffset === 0 ? packDataPoints(data) : packDataPointsWithXOffset(data, xOffset); |
| 138 | const pointCount = data.length; |
| 139 | const hash32 = hashFloat32ArrayBits(packed); |
| 140 | |
| 141 | const requiredBytes = roundUpToMultipleOf4(packed.byteLength); |
| 142 | const targetBytes = Math.max(MIN_BUFFER_BYTES, requiredBytes); |
| 143 | |
| 144 | const existing = series.get(index); |
| 145 | const unchanged = existing && existing.pointCount === pointCount && existing.hash32 === hash32; |
| 146 | if (unchanged) return; |
| 147 | |
| 148 | let buffer = existing?.buffer ?? null; |
no outgoing calls
no test coverage detected