/** * 도면에서 받침 ELEVATION TABLE 읽기 (승인 2026-10-23). Reads the TEXT entities of a DXF file and places each value * by position: the header row names the columns (EL.X, h1 … h4, A, B, C1 …), the first column holds the bearing No. * Pure: works on the text entities (x, y, text) of the drawing, not on the file itself. */ import type { DrawingBearing } from './bearing-ref'; export interface DxfText { x: number; y: number; text: string; } /** Reads TEXT and MTEXT entities (x, y, string) from DXF text. */ export function readDxfTexts(dxf: string): DxfText[] { const lines = dxf.split(/\r?\n/); const pairs: [string, string][] = []; for (let i = 0; i + 1 < lines.length; i += 2) pairs.push([lines[i].trim(), lines[i + 1].trim()]); const out: DxfText[] = []; for (let i = 0; i < pairs.length; i++) { if (pairs[i][0] !== '0' || (pairs[i][1] !== 'TEXT' && pairs[i][1] !== 'MTEXT')) continue; let text: string | null = null; let x = NaN; let y = NaN; let j = i + 1; for (; j < pairs.length && pairs[j][0] !== '0'; j++) { const [c, v] = pairs[j]; if (c === '1') text = v; if (c === '10') x = Number(v); if (c === '20') y = Number(v); } if (text !== null && Number.isFinite(x) && Number.isFinite(y)) out.push({ x, y, text }); i = j - 1; } return out; } const norm = (s: string) => s.replace(/[\s.'"]/g, '').toLowerCase(); const NUMBER = /^-?\d+(\.\d+)?$/; export interface BearingTableRow { no: number; values: Record; } export interface BearingTableResult { /** Values per column as read from the drawing (keyed by header name). */ rows: BearingTableRow[]; /** The rows in the form the 받침 입력 uses (EL.X in m, h1 and h2 in mm). */ drawing: DrawingBearing[]; errors: string[]; } /** * Reads the bearing ELEVATION TABLE. The header is the text row with "EL.X" (as EL.'X', EL. "X"); the No column is the * integer column just left of the 교량받침 column; each value sits under its header (x within 150) on the same row (y within 120). */ export function parseBearingElevationTable(texts: DxfText[]): BearingTableResult { const errors: string[] = []; const elHeader = texts.find((t) => norm(t.text) === 'el' + 'x' || norm(t.text) === 'elx'); if (!elHeader) return { rows: [], drawing: [], errors: ['EL.X 머리글을 찾지 못했습니다.'] }; const headerY = elHeader.y; // Column headers: texts on the header row (within 60 of its y) with a short name (EL.X, h1 … h4, A, B, C1 …). const headerRow = texts.filter((t) => Math.abs(t.y - headerY) <= 60 && /^(elx|h\d|[a-z]\d?)$/.test(norm(t.text))); const cols = headerRow.map((t) => ({ key: norm(t.text), x: t.x })); const noHeader = texts.find((t) => norm(t.text) === 'no'); // The No column: integers 1…10 below the header, within 150 of the No header (or the left-most integer column). const numbers = texts.filter((t) => NUMBER.test(t.text) && Number.isInteger(Number(t.text)) && Number(t.text) >= 1 && Number(t.text) <= 99 && t.y < headerY - 60); if (numbers.length === 0) return { rows: [], drawing: [], errors: ['받침 번호 열을 찾지 못했습니다.'] }; const noX = noHeader ? noHeader.x : Math.min(...numbers.map((t) => t.x)); const noCells = numbers.filter((t) => Math.abs(t.x - noX) <= 250).sort((a, b) => b.y - a.y); const rows: BearingTableRow[] = []; for (const cell of noCells) { const values: Record = {}; for (const col of cols) { const hit = texts.find((t) => Math.abs(t.x - col.x) <= 150 && Math.abs(t.y - cell.y) <= 120 && NUMBER.test(t.text)); if (hit) values[col.key] = Number(hit.text); } rows.push({ no: Number(cell.text), values }); } const drawing: DrawingBearing[] = []; for (const r of rows) { const elx = r.values.elx; const h1 = r.values.h1; const h2 = r.values.h2; if (elx === undefined || h1 === undefined || h2 === undefined) { errors.push(`No ${r.no}: EL.X·h1·h2 중 값이 없습니다.`); continue; } drawing.push({ no: r.no, elx, h1, h2 }); } return { rows, drawing: drawing.sort((a, b) => a.no - b.no), errors }; }