diff --git a/src/core/dxf-table.ts b/src/core/dxf-table.ts index 7ce2d4b..17c3f15 100644 --- a/src/core/dxf-table.ts +++ b/src/core/dxf-table.ts @@ -57,13 +57,21 @@ export interface BearingTableResult { */ 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 머리글을 찾지 못했습니다.'] }; + // The drawing has other "EL.X" labels (leader lines). The table header is the one with h1 and h2 on the same row. + // The header is the EL.X label with the most table column names on its row, to its right (within 2500). + const TABLE_NAMES = ['h1', 'h2', 'h3', 'h4', 'a', 'b', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 's1', 's2']; + const rowScore = (t: DxfText) => + texts.filter((u) => Math.abs(u.y - t.y) <= 60 && u.x > t.x && u.x - t.x <= 2500 && TABLE_NAMES.includes(norm(u.text))).length; + const candidates = texts.filter((t) => norm(t.text) === 'elx'); + const best = candidates.map((t) => ({ t, score: rowScore(t) })).sort((a, b) => b.score - a.score)[0]; + if (!best || best.score < 2) return { rows: [], drawing: [], errors: ['EL.X 머리글(표의 열 이름과 같은 행)을 찾지 못했습니다.'] }; + const elHeader = best.t; 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 header is the 'NO' label on the table's header row (the drawing has other 'NO' labels). + const noHeader = texts.find((t) => norm(t.text) === 'no' && Math.abs(t.y - headerY) <= 200); // 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: ['받침 번호 열을 찾지 못했습니다.'] }; diff --git a/tests/dxf-table.test.ts b/tests/dxf-table.test.ts index 8227df2..3770708 100644 --- a/tests/dxf-table.test.ts +++ b/tests/dxf-table.test.ts @@ -22,6 +22,19 @@ describe('도면 받침 ELEVATION TABLE 읽기 (DXF 텍스트, 승인 2026-10-23 } }); + it('a leader-line "EL. X" label placed before the table does not replace the table header', () => { + const distractor = { x: 11102.8, y: 11521.7, text: 'EL. "X"' }; + const r = parseBearingElevationTable([distractor, ...texts]); + expect(r.errors).toEqual([]); + expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('a stray "NO" label elsewhere on the drawing does not move the No column', () => { + const r = parseBearingElevationTable([{ x: 100, y: 100, text: 'NO.' }, ...texts]); + expect(r.errors).toEqual([]); + expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + it('reads TEXT entities from DXF text (x, y, string)', () => { const dxf = ['0', 'SECTION', '2', 'ENTITIES', '0', 'TEXT', '10', '100.5', '20', '200.25', '1', 'EL. "X"', '0', 'ENDSEC'].join('\n'); expect(readDxfTexts(dxf)).toEqual([{ x: 100.5, y: 200.25, text: 'EL. "X"' }]);