diff --git a/src/core/dxf-table.ts b/src/core/dxf-table.ts new file mode 100644 index 0000000..7ce2d4b --- /dev/null +++ b/src/core/dxf-table.ts @@ -0,0 +1,94 @@ +/** + * 도면에서 받침 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 }; +} diff --git a/src/main.ts b/src/main.ts index ec694a7..16819d7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -16,6 +16,7 @@ import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar import { DEFAULT_MATERIAL, FCK_MEMBERS, fyOf, GRADE_CHOICES, isAssumedMaterial, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common'; import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity'; import { parseDrawingRows } from './core/drawing-import'; +import { parseBearingElevationTable, readDxfTexts } from './core/dxf-table'; import { P1_COPING_GEOM } from './core/coping-geom'; import { applyBearingRules, BEARING_ELEVATION_REF, BEARING_RULES_DEFAULT, type BearingBasis, type BearingRules, type DrawingBearing, DRAWING_TOL_MM, drawingCopingCheck, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs, withDrawingBasis } from './core/bearing-ref'; @@ -1341,24 +1342,36 @@ function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void { // 도면 불러오기 (승인 2026-10-23): a sheet with No, EL.X, h1, h2 (xlsx or csv) fills this pier's drawing values. const importInput = el('input'); importInput.type = 'file'; - importInput.accept = '.xlsx,.xls,.csv'; + importInput.accept = '.xlsx,.xls,.csv,.dxf'; importInput.id = 'drawing-import'; importInput.addEventListener('change', async () => { const file = importInput.files?.[0]; if (!file) return; try { - const wb = XLSX.read(await file.arrayBuffer(), { type: 'array' }); - const sheet = wb.Sheets[wb.SheetNames[0]]; - const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, blankrows: false, defval: '' }); - const result = parseDrawingRows(rows); - if (result.errors.length) { - setError(`도면 불러오기 실패: ${result.errors.slice(0, 3).join(' ')}`); - return; + // A DXF drawing: the 교량받침 ELEVATION TABLE is read by position. Otherwise a sheet with No, EL.X, h1, h2. + let drawingRows: DrawingBearing[]; + if (file.name.toLowerCase().endsWith('.dxf')) { + const r = parseBearingElevationTable(readDxfTexts(await file.text())); + if (r.errors.length) { + setError(`도면 불러오기 실패: ${r.errors.slice(0, 3).join(' ')}`); + return; + } + drawingRows = r.drawing; + } else { + const wb = XLSX.read(await file.arrayBuffer(), { type: 'array' }); + const sheet = wb.Sheets[wb.SheetNames[0]]; + const rows = XLSX.utils.sheet_to_json(sheet, { header: 1, blankrows: false, defval: '' }); + const r = parseDrawingRows(rows); + if (r.errors.length) { + setError(`도면 불러오기 실패: ${r.errors.slice(0, 3).join(' ')}`); + return; + } + drawingRows = r.rows; } - bearingDrawing = { ...bearingDrawing, [name]: result.rows }; + bearingDrawing = { ...bearingDrawing, [name]: drawingRows }; regenerateAllPiers(); renderAll(); - setStatus(`${file.name}에서 받침 ${result.rows.length}개의 도면 값을 불러왔습니다.`); + setStatus(`${file.name}에서 받침 ${drawingRows.length}개의 도면 값을 불러왔습니다.`); } catch (err) { setError(`도면 파일을 읽지 못했습니다: ${(err as Error).message}`); } finally { diff --git a/tests/dxf-table.test.ts b/tests/dxf-table.test.ts new file mode 100644 index 0000000..8227df2 --- /dev/null +++ b/tests/dxf-table.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; +import { readFileSync } from 'node:fs'; +import { BEARING_ELEVATION_REF } from '../src/core/bearing-ref'; +import { parseBearingElevationTable, readDxfTexts, type DxfText } from '../src/core/dxf-table'; + +const texts = JSON.parse(readFileSync(new URL('./fixtures/bearing-table-texts.json', import.meta.url), 'utf-8')) as DxfText[]; + +describe('도면 받침 ELEVATION TABLE 읽기 (DXF 텍스트, 승인 2026-10-23)', () => { + it('reads all ten bearings from the 운곡교 교각도(2) table', () => { + const r = parseBearingElevationTable(texts); + expect(r.errors).toEqual([]); + expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + }); + + it('the values equal the reference table used for the 도면 비교 (EL.X, h1, h2 on every bearing)', () => { + const r = parseBearingElevationTable(texts); + for (const ref of BEARING_ELEVATION_REF) { + const got = r.drawing.find((d) => d.no === ref.no)!; + expect(got.elx, `No ${ref.no} EL.X`).toBeCloseTo(ref.elx, 3); + expect(got.h1, `No ${ref.no} h1`).toBe(ref.h1); + expect(got.h2, `No ${ref.no} h2`).toBe(ref.h2); + } + }); + + 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"' }]); + }); +}); diff --git a/tests/fixtures/bearing-table-texts.json b/tests/fixtures/bearing-table-texts.json new file mode 100644 index 0000000..9d6f9ea --- /dev/null +++ b/tests/fixtures/bearing-table-texts.json @@ -0,0 +1,667 @@ +[ +{ +"x": 26178.57784142365, +"y": 9341.774999716086, +"text": "교량받침 ELEVATION TABLE" +}, +{ +"x": 21491.64556026707, +"y": 8694.549697067356, +"text": "NO." +}, +{ +"x": 23035.9864390953, +"y": 8694.549697067356, +"text": "교량받침" +}, +{ +"x": 24938.250087564, +"y": 8694.549697067356, +"text": "EL.'X'" +}, +{ +"x": 26192.77738450142, +"y": 8694.549697067356, +"text": "h1" +}, +{ +"x": 27092.77738450142, +"y": 8694.549697067356, +"text": "h2" +}, +{ +"x": 27992.77738450142, +"y": 8694.549697067356, +"text": "h3" +}, +{ +"x": 28892.77738450142, +"y": 8694.549697067356, +"text": "h4" +}, +{ +"x": 29843.90920873577, +"y": 8694.549697067356, +"text": "A" +}, +{ +"x": 30743.90920873577, +"y": 8694.549697067356, +"text": "B" +}, +{ +"x": 31592.77738450142, +"y": 8694.549697067356, +"text": "C1" +}, +{ +"x": 32492.77738450142, +"y": 8694.549697067356, +"text": "C2" +}, +{ +"x": 33392.77738450142, +"y": 8694.549697067356, +"text": "C3" +}, +{ +"x": 21593.90920873577, +"y": 8214.549697067356, +"text": "1" +}, +{ +"x": 22729.19549368917, +"y": 8214.549697067356, +"text": "3500 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 8214.549697067356, +"text": "43.625" +}, +{ +"x": 26141.64556026707, +"y": 8214.549697067356, +"text": "164" +}, +{ +"x": 27092.77738450142, +"y": 8214.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 8214.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 8214.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 8214.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 8214.549697067356, +"text": "640" +}, +{ +"x": 31541.64556026707, +"y": 8214.549697067356, +"text": "600" +}, +{ +"x": 32390.51373603271, +"y": 8214.549697067356, +"text": "1298" +}, +{ +"x": 33290.51373603271, +"y": 8214.549697067356, +"text": "2500" +}, +{ +"x": 21593.90920873577, +"y": 7734.549697067356, +"text": "2" +}, +{ +"x": 22729.19549368917, +"y": 7734.549697067356, +"text": "3500 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 7734.549697067356, +"text": "43.697" +}, +{ +"x": 26141.64556026707, +"y": 7734.549697067356, +"text": "164" +}, +{ +"x": 27092.77738450142, +"y": 7734.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 7734.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 7734.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 7734.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 7734.549697067356, +"text": "640" +}, +{ +"x": 31541.64556026707, +"y": 7734.549697067356, +"text": "600" +}, +{ +"x": 32390.51373603271, +"y": 7734.549697067356, +"text": "1298" +}, +{ +"x": 33290.51373603271, +"y": 7734.549697067356, +"text": "2500" +}, +{ +"x": 21593.90920873577, +"y": 7254.549697067356, +"text": "3" +}, +{ +"x": 22626.93184522046, +"y": 7254.549697067356, +"text": "3500 kN 교축방향" +}, +{ +"x": 24938.250087564, +"y": 7254.549697067356, +"text": "43.768" +}, +{ +"x": 26141.64556026707, +"y": 7254.549697067356, +"text": "164" +}, +{ +"x": 27092.77738450142, +"y": 7254.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 7254.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 7254.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 7254.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 7254.549697067356, +"text": "790" +}, +{ +"x": 31541.64556026707, +"y": 7254.549697067356, +"text": "600" +}, +{ +"x": 32390.51373603271, +"y": 7254.549697067356, +"text": "1298" +}, +{ +"x": 33290.51373603271, +"y": 7254.549697067356, +"text": "2500" +}, +{ +"x": 21593.90920873577, +"y": 6774.549697067356, +"text": "4" +}, +{ +"x": 22729.19549368917, +"y": 6774.549697067356, +"text": "3500 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 6774.549697067356, +"text": "43.840" +}, +{ +"x": 26141.64556026707, +"y": 6774.549697067356, +"text": "164" +}, +{ +"x": 27092.77738450142, +"y": 6774.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 6774.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 6774.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 6774.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 6774.549697067356, +"text": "640" +}, +{ +"x": 31541.64556026707, +"y": 6774.549697067356, +"text": "600" +}, +{ +"x": 32390.51373603271, +"y": 6774.549697067356, +"text": "1298" +}, +{ +"x": 33290.51373603271, +"y": 6774.549697067356, +"text": "2500" +}, +{ +"x": 21593.90920873577, +"y": 6294.549697067356, +"text": "5" +}, +{ +"x": 22729.19549368917, +"y": 6294.549697067356, +"text": "3500 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 6294.549697067356, +"text": "43.912" +}, +{ +"x": 26141.64556026707, +"y": 6294.549697067356, +"text": "164" +}, +{ +"x": 27092.77738450142, +"y": 6294.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 6294.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 6294.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 6294.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 6294.549697067356, +"text": "640" +}, +{ +"x": 31541.64556026707, +"y": 6294.549697067356, +"text": "600" +}, +{ +"x": 32390.51373603271, +"y": 6294.549697067356, +"text": "1298" +}, +{ +"x": 33290.51373603271, +"y": 6294.549697067356, +"text": "2500" +}, +{ +"x": 21593.90920873577, +"y": 5814.549697067356, +"text": "6" +}, +{ +"x": 22729.19549368917, +"y": 5814.549697067356, +"text": "2800 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 5814.549697067356, +"text": "43.600" +}, +{ +"x": 26141.64556026707, +"y": 5814.549697067356, +"text": "139" +}, +{ +"x": 27092.77738450142, +"y": 5814.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 5814.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 5814.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 5814.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 5814.549697067356, +"text": "540" +}, +{ +"x": 31541.64556026707, +"y": 5814.549697067356, +"text": "602" +}, +{ +"x": 32543.90920873577, +"y": 5814.549697067356, +"text": "-" +}, +{ +"x": 33443.90920873577, +"y": 5814.549697067356, +"text": "-" +}, +{ +"x": 21593.90920873577, +"y": 5334.549697067356, +"text": "7" +}, +{ +"x": 22729.19549368917, +"y": 5334.549697067356, +"text": "2800 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 5334.549697067356, +"text": "43.672" +}, +{ +"x": 26141.64556026707, +"y": 5334.549697067356, +"text": "139" +}, +{ +"x": 27092.77738450142, +"y": 5334.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 5334.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 5334.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 5334.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 5334.549697067356, +"text": "540" +}, +{ +"x": 31541.64556026707, +"y": 5334.549697067356, +"text": "602" +}, +{ +"x": 32543.90920873577, +"y": 5334.549697067356, +"text": "-" +}, +{ +"x": 33443.90920873577, +"y": 5334.549697067356, +"text": "-" +}, +{ +"x": 21593.90920873577, +"y": 4854.549697067356, +"text": "8" +}, +{ +"x": 22626.93184522046, +"y": 4854.549697067356, +"text": "2800 kN 교축방향" +}, +{ +"x": 24938.250087564, +"y": 4854.549697067356, +"text": "43.744" +}, +{ +"x": 26141.64556026707, +"y": 4854.549697067356, +"text": "139" +}, +{ +"x": 27092.77738450142, +"y": 4854.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 4854.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 4854.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 4854.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 4854.549697067356, +"text": "650" +}, +{ +"x": 31541.64556026707, +"y": 4854.549697067356, +"text": "602" +}, +{ +"x": 32543.90920873577, +"y": 4854.549697067356, +"text": "-" +}, +{ +"x": 33443.90920873577, +"y": 4854.549697067356, +"text": "-" +}, +{ +"x": 21593.90920873577, +"y": 4374.549697067356, +"text": "9" +}, +{ +"x": 22729.19549368917, +"y": 4374.549697067356, +"text": "2800 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 4374.549697067356, +"text": "43.816" +}, +{ +"x": 26141.64556026707, +"y": 4374.549697067356, +"text": "139" +}, +{ +"x": 27092.77738450142, +"y": 4374.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 4374.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 4374.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 4374.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 4374.549697067356, +"text": "540" +}, +{ +"x": 31541.64556026707, +"y": 4374.549697067356, +"text": "602" +}, +{ +"x": 32543.90920873577, +"y": 4374.549697067356, +"text": "-" +}, +{ +"x": 33443.90920873577, +"y": 4374.549697067356, +"text": "-" +}, +{ +"x": 21542.77738450142, +"y": 3894.549697067356, +"text": "10" +}, +{ +"x": 22729.19549368917, +"y": 3894.549697067356, +"text": "2800 kN 양방향" +}, +{ +"x": 24938.250087564, +"y": 3894.549697067356, +"text": "43.887" +}, +{ +"x": 26141.64556026707, +"y": 3894.549697067356, +"text": "139" +}, +{ +"x": 27092.77738450142, +"y": 3894.549697067356, +"text": "50" +}, +{ +"x": 27941.64556026707, +"y": 3894.549697067356, +"text": "181" +}, +{ +"x": 28892.77738450142, +"y": 3894.549697067356, +"text": "30" +}, +{ +"x": 29741.64556026707, +"y": 3894.549697067356, +"text": "700" +}, +{ +"x": 30641.64556026707, +"y": 3894.549697067356, +"text": "540" +}, +{ +"x": 31541.64556026707, +"y": 3894.549697067356, +"text": "602" +}, +{ +"x": 32543.90920873577, +"y": 3894.549697067356, +"text": "-" +}, +{ +"x": 33443.90920873577, +"y": 3894.549697067356, +"text": "-" +} +] \ No newline at end of file