import type { Well, WellPlateFormat, WellPlateSize } from '../types' export interface WellPlateConfig { rows: number cols: number } export interface WellPlateCoordinate { row: number col: number } export interface WellPlateSizeConfig { cellWidth: string cellHeight: string headerWidth: string headerHeight: string fontSize: string gap: string } export const PLATE_CONFIGS: Record = { 6: { rows: 2, cols: 3 }, 12: { rows: 3, cols: 4 }, 24: { rows: 4, cols: 6 }, 48: { rows: 6, cols: 8 }, 54: { rows: 6, cols: 9 }, 96: { rows: 8, cols: 12 }, 384: { rows: 16, cols: 24 }, } export const WELL_PLATE_SIZE_CONFIGS: Record = { // Compact square wells (PlateEditor, 7a): room for the Q / i glyph, not a sample name. xs: { cellWidth: '26px', cellHeight: '26px', headerWidth: '20px', headerHeight: '20px', fontSize: '0.6875rem', gap: '6px' }, sm: { cellWidth: '40px', cellHeight: '40px', headerWidth: '32px', headerHeight: '32px', fontSize: '0.625rem', gap: '2px' }, md: { cellWidth: '56px', cellHeight: '32px', headerWidth: '32px', headerHeight: '24px', fontSize: '0.75rem', gap: '3px' }, lg: { cellWidth: '80px', cellHeight: '40px', headerWidth: '40px', headerHeight: '32px', fontSize: '0.875rem', gap: '4px' }, xl: { cellWidth: '96px', cellHeight: '48px', headerWidth: '48px', headerHeight: '40px', fontSize: '1rem', gap: '4px' }, fill: { cellWidth: '100%', cellHeight: '32px', headerWidth: '32px', headerHeight: '24px', fontSize: '0.75rem', gap: '2px' }, } /** * Spreadsheet-style row label for a zero-based row index: A…Z, then AA, AB, … * * This is the single row-label generator for the SDK — plate grids, rack * grids, well-id parsing and the template builders all go through it. */ export function rowLabel(index: number): string { let label = '' let current = index for (;;) { label = String.fromCharCode(65 + (current % 26)) + label current = Math.floor(current / 26) - 1 if (current < 0) return label } } /** Inverse of {@link rowLabel}: 'A' → 0, 'Z' → 25, 'AA' → 26. */ export function rowIndex(label: string): number { return [...label].reduce((value, char) => value * 26 + char.charCodeAt(0) - 64, 0) - 1 } export function createPlateRowLabels(config: WellPlateConfig): string[] { return Array.from({ length: config.rows }, (_, index) => rowLabel(index)) } function plateConfigOrThrow(format: number): WellPlateConfig { const config = PLATE_CONFIGS[format as WellPlateFormat] if (!config) throw new Error(`Unsupported plate format '${format}'.`) return config } /** Every well id of a plate, row-major (A1, A2, … B1, …). */ export function wellIdsForFormat(format: number): string[] { const { rows, cols } = plateConfigOrThrow(format) return Array.from({ length: rows }, (_, row) => Array.from({ length: cols }, (_col, col) => `${rowLabel(row)}${col + 1}`) ).flat() } /** Throw unless `wellId` names a well that exists on a `format`-well plate. */ export function assertWellInFormat(wellId: string, format: number): void { const { rows, cols } = plateConfigOrThrow(format) const match = /^([A-Z]+)([1-9][0-9]*)$/.exec(wellId.toUpperCase()) if (!match) { throw new Error(`Invalid well id '${wellId}'.`) } const row = rowIndex(match[1]) const col = Number(match[2]) - 1 if (row < 0 || col < 0 || row >= rows || col >= cols) { throw new Error(`Well '${wellId}' is outside a ${format}-well plate.`) } } /** Coerce an unknown value to a supported plate format, else `fallback`. */ export function readPlateFormat(value: unknown, fallback: WellPlateFormat): WellPlateFormat { const parsed = typeof value === 'number' ? value : typeof value === 'string' ? Number(value) : Number.NaN return parsed in PLATE_CONFIGS ? (parsed as WellPlateFormat) : fallback } export function createPlateColumnLabels(config: WellPlateConfig): number[] { return Array.from({ length: config.cols }, (_, index) => index + 1) } export function createWellGrid( config: WellPlateConfig, rowLabels: string[], wells: Record>, ): Well[][] { const grid: Well[][] = [] for (let row = 0; row < config.rows; row++) { const rowWells: Well[] = [] for (let col = 0; col < config.cols; col++) { const id = `${rowLabels[row]}${col + 1}` const wellData = wells[id] || {} rowWells.push({ id, row, col, state: wellData.state || 'empty', sampleType: wellData.sampleType, group: wellData.group, value: wellData.value, metadata: wellData.metadata, }) } grid.push(rowWells) } return grid } export function getRectangleWellIds( start: WellPlateCoordinate, end: WellPlateCoordinate, rowLabels: string[], ): Set { const minRow = Math.min(start.row, end.row) const maxRow = Math.max(start.row, end.row) const minCol = Math.min(start.col, end.col) const maxCol = Math.max(start.col, end.col) const wells = new Set() for (let row = minRow; row <= maxRow; row++) { for (let col = minCol; col <= maxCol; col++) { wells.add(`${rowLabels[row]}${col + 1}`) } } return wells }