import type { FlatKey } from "./phy"; import type { ProjectFile } from "./project"; import { detectColumns, detectRows, type KeyRect } from "./rows"; /** Gaps narrower than this do not separate the two halves. T-31. */ const MIN_HALF_GAP = 0.5; export type ScanKind = "rows" | "columns" | "halves" | "alternating"; export type Direction = "normal" | "reversed"; export type ScanOptions = { kind: ScanKind; /** Whole-board direction for `rows` and `columns`. */ direction: Direction; /** Per-half directions for `halves` and `alternating`. */ left: Direction; right: Direction; /** Shift of the line assignment per cross line index (columns for row scans, rows for `columns`). */ offsets: ReadonlyMap; }; export type ScanResult = { sequence: string[]; /** Lines the scan walks, for the preview: rows, or columns for `columns`. */ lines: string[][]; /** Lines that can be shifted with `offsets`, in index order. */ crossLines: string[][]; /** x of the split between the halves, for scans that use halves. */ boundary?: number; }; /** Axis-aligned bounding box of a key in world coordinates; rotated keys are judged by it. T-31. */ export function keyRect(k: FlatKey): KeyRect { const r = (k.rotation * Math.PI) / 180, c = Math.cos(r), s = Math.sin(r); const { w, h } = k.key.size; const xs = [0, w * c, -h * s, w * c - h * s].map((dx) => k.x + dx); const ys = [0, w * s, h * c, w * s + h * c].map((dy) => k.y + dy); const x = Math.min(...xs), y = Math.min(...ys); return { id: k.key.id, x, y, w: Math.max(...xs) - x, h: Math.max(...ys) - y }; } /** * Splits keys into left and right halves at the widest gap between detected columns. * When no gap reaches MIN_HALF_GAP, splits at the middle of the keys' x range by key center. */ export function splitHalves(rects: KeyRect[]): { left: Set; right: Set; boundary: number } { const columns = detectColumns(rects); const byId = new Map(rects.map((r) => [r.id, r])); const span = columns.map((col) => { const rs = col.map((id) => byId.get(id)!); return { min: Math.min(...rs.map((r) => r.x)), max: Math.max(...rs.map((r) => r.x + r.w)) }; }); let best = -1, width = -Infinity; for (let i = 0; i + 1 < span.length; i++) { const gap = span[i + 1]!.min - span[i]!.max; if (gap > width) [best, width] = [i, gap]; } if (best >= 0 && width >= MIN_HALF_GAP) { const left = new Set(columns.slice(0, best + 1).flat()); return { left, right: new Set(rects.map((r) => r.id).filter((id) => !left.has(id))), boundary: (span[best]!.max + span[best + 1]!.min) / 2, }; } const boundary = rects.length ? (Math.min(...rects.map((r) => r.x)) + Math.max(...rects.map((r) => r.x + r.w))) / 2 : 0; const left = new Set(rects.filter((r) => r.x + r.w / 2 < boundary).map((r) => r.id)); return { left, right: new Set(rects.map((r) => r.id).filter((id) => !left.has(id))), boundary }; } /** * Detects rows (or columns), then moves every key of cross line `i` by `offsets.get(i)` lines. * Rows are rebuilt top to bottom and sorted by x; columns left to right and sorted by y. */ export function assignLines( rects: KeyRect[], kind: "row" | "column", offsets: ReadonlyMap, ): { lines: string[][]; crossLines: string[][] } { const base = kind === "row" ? detectRows(rects) : detectColumns(rects); const crossLines = kind === "row" ? detectColumns(rects) : detectRows(rects); const line = new Map(base.flatMap((ids, i) => ids.map((id) => [id, i] as const))); crossLines.forEach((ids, i) => ids.forEach((id) => line.set(id, line.get(id)! + (offsets.get(i) ?? 0)))); const byId = new Map(rects.map((r) => [r.id, r])); const along = (id: string) => (kind === "row" ? byId.get(id)!.x : byId.get(id)!.y); const indexes = [...new Set(line.values())].sort((a, b) => a - b); const lines = indexes.map((i) => [...line] .filter(([, at]) => at === i) .map(([id]) => id) .sort((a, b) => along(a) - along(b)), ); return { lines, crossLines }; } const directed = (items: T[], d: Direction) => (d === "reversed" ? [...items].reverse() : items); /** Builds the key sequence of a preset scan over the target keys. T-31. */ export function scanKeys(rects: KeyRect[], options: ScanOptions): ScanResult { if (options.kind === "columns") { const { lines, crossLines } = assignLines(rects, "column", options.offsets); return { sequence: directed(lines, options.direction).flat(), lines, crossLines }; } const { lines, crossLines } = assignLines(rects, "row", options.offsets); if (options.kind === "rows") return { sequence: lines.flatMap((row) => directed(row, options.direction)), lines, crossLines }; const { left, boundary } = splitHalves(rects); const leftPart = (row: string[]) => directed( row.filter((id) => left.has(id)), options.left, ); const rightPart = (row: string[]) => directed( row.filter((id) => !left.has(id)), options.right, ); const sequence = options.kind === "halves" ? [...lines.flatMap(leftPart), ...lines.flatMap(rightPart)] : lines.flatMap((row) => [...leftPart(row), ...rightPart(row)]); return { sequence, lines, crossLines, boundary }; } /** * Finds each contact key's (row, col) from the matrix pins, by the same rule as the Generator. * Returns undefined when the project has no matrix settings. T-19. */ export function matrixPositions(p: ProjectFile): Map | undefined { const matrix = p.output.matrix; if (!matrix) return undefined; const rowIndex = new Map(matrix.rowPins.map((x, i) => [x, i])), colIndex = new Map(matrix.colPins.map((x, i) => [x, i])); const out = new Map(); for (const w of p.elc.keys) { if (w.detect !== "contact") continue; const { a, b } = w.terminals; if (rowIndex.has(a) && colIndex.has(b)) out.set(w.key, { row: rowIndex.get(a)!, col: colIndex.get(b)! }); else if (rowIndex.has(b) && colIndex.has(a)) out.set(w.key, { row: rowIndex.get(b)!, col: colIndex.get(a)! }); } return out; } /** Sorts keys by row then col. Keys without a matrix position keep their input order at the end. */ export function matrixSequence( ids: readonly string[], positions: ReadonlyMap, ) { const placed = ids.filter((id) => positions.has(id)); const rest = ids.filter((id) => !positions.has(id)); const key = (id: string) => positions.get(id)!; return [...placed.sort((a, b) => key(a).row - key(b).row || key(a).col - key(b).col), ...rest]; }