feat(view): read the bearing ELEVATION TABLE from a DXF drawing (교각도(2) P1) by position

The reader takes TEXT and MTEXT entities (x, y, text). The EL.X header row names the columns; the No column holds the
bearing numbers; each value is the nearest entry under its header on the same row. On the 운곡교 교각도(2) drawing all ten
bearings read back the reference values (EL.X, h1, h2). The 도면 불러오기 button accepts .dxf as well as sheets. The test
fixture holds only the table's text entries.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-10-23 14:34:33 +09:00
1 parent 6f32bd96c4
commit 4048c326e3
4 files changed
+813 -10

No files matched your search

+94
View File
@@ -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<string, number>;
}
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<string, number> = {};
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 };
}
+23 -10
View File
@@ -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<unknown[]>(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<unknown[]>(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 {
+29
View File
@@ -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"' }]);
});
});
+667
View File
@@ -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": "-"
}
]