feat(core): parse P1 workbook and build T-type pier shape
- parse Excel input by header strings (pier, IP, bearing tables) - DMS azimuth parser (E03), input validation (E01, E02) - pure geometry: axis, bearings, stepped coping, column, footing, piles, blinding - warnings W01, W02, W05, W07, I01 - regression tests against spec.md section 12 (P1) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
d963d8ed3c
15 files changed
+2527
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import * as XLSX from 'xlsx';
|
||||
import { parseDms } from './dms';
|
||||
import type { Bearing, FoundationType, Issue, IpPoint, PierInput, PierType, Row } from './types';
|
||||
|
||||
type Cell = string | number | null | undefined;
|
||||
type Grid = Cell[][];
|
||||
|
||||
export interface ParseResult {
|
||||
piers: PierInput[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
const PIER_TYPES: PierType[] = ['T형', '2주형', '3주형'];
|
||||
const FOUNDATION_TYPES: FoundationType[] = ['말뚝기초', '직접기초', '현장타설말뚝'];
|
||||
|
||||
/** Reads every sheet and collects the pier tables found by their header strings (position-independent). */
|
||||
export function parseWorkbook(wb: XLSX.WorkBook): ParseResult {
|
||||
const piers: PierInput[] = [];
|
||||
const issues: Issue[] = [];
|
||||
for (const sheetName of wb.SheetNames) {
|
||||
const ws = wb.Sheets[sheetName];
|
||||
const grid = XLSX.utils.sheet_to_json<Cell[]>(ws, { header: 1, raw: true, defval: null }) as Grid;
|
||||
piers.push(...parseSheet(grid, sheetName, issues));
|
||||
}
|
||||
return { piers, issues };
|
||||
}
|
||||
|
||||
function parseSheet(grid: Grid, sheet: string, issues: Issue[]): PierInput[] {
|
||||
const pierHeaders = findHeaders(grid, (r) => r[0] === '교각명' && r[1] === '교각형식');
|
||||
const ipHeaders = findHeaders(grid, (r) => r[0] === '교각명' && r[1] === 'IP점');
|
||||
const bearingHeaders = findHeaders(grid, (r) => r[0] === '교각명' && r[1] === '받침번호');
|
||||
|
||||
const ips = ipHeaders.flatMap((h) => readBlock(grid, h).map((r) => toIp(r)));
|
||||
const bearings: { pier: string; bearing: Bearing; row: number; azimuthOk: boolean }[] = [];
|
||||
for (const h of bearingHeaders) {
|
||||
for (const { cells, index } of readBlock(grid, h)) {
|
||||
const azimuth = parseDms(String(cells[5] ?? ''));
|
||||
bearings.push({
|
||||
pier: String(cells[0]),
|
||||
row: index + 1,
|
||||
azimuthOk: azimuth !== null,
|
||||
bearing: {
|
||||
no: Number(cells[1]),
|
||||
x: Number(cells[2]),
|
||||
y: Number(cells[3]),
|
||||
z: Number(cells[4]),
|
||||
azimuthDeg: azimuth ?? NaN,
|
||||
row: String(cells[6]) as Row,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const piers: PierInput[] = [];
|
||||
for (const h of pierHeaders) {
|
||||
for (const { cells, index } of readBlock(grid, h)) {
|
||||
const name = String(cells[0]);
|
||||
const bad = bearings.find((b) => b.pier === name && !b.azimuthOk);
|
||||
if (bad) {
|
||||
issues.push({
|
||||
code: 'E03',
|
||||
level: 'E',
|
||||
pier: name,
|
||||
message: `${sheet} ${bad.row}행: 방위각 문자열을 읽을 수 없습니다. 예: 75°56'55.9"`,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const type = String(cells[1]) as PierType;
|
||||
const foundation = String(cells[5]) as FoundationType;
|
||||
if (!PIER_TYPES.includes(type) || !FOUNDATION_TYPES.includes(foundation)) {
|
||||
issues.push({ code: 'E01', level: 'E', pier: name, message: `${sheet} ${index + 1}행: 교각형식 또는 기초 형식을 알 수 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
piers.push({
|
||||
name,
|
||||
type,
|
||||
copingTopEl: Number(cells[2]),
|
||||
footingBottomEl: Number(cells[3]),
|
||||
groundEl: Number(cells[4]),
|
||||
foundationType: foundation,
|
||||
pileLengthMm: Number(cells[6]),
|
||||
copingWidthMm: Number(cells[7]),
|
||||
copingLengthMm: Number(cells[8]),
|
||||
columnCount: Number(cells[9]),
|
||||
ips: ips.filter((p) => p.pier === name).map((p) => p.ip),
|
||||
bearings: bearings.filter((b) => b.pier === name).map((b) => b.bearing),
|
||||
});
|
||||
}
|
||||
}
|
||||
return piers;
|
||||
}
|
||||
|
||||
function findHeaders(grid: Grid, match: (row: Cell[]) => boolean): number[] {
|
||||
const out: number[] = [];
|
||||
grid.forEach((row, i) => {
|
||||
if (match(row)) out.push(i);
|
||||
});
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Reads the data rows under a header until the first blank row. Blank pier-name cells inherit the row above. */
|
||||
function readBlock(grid: Grid, header: number): { cells: Cell[]; index: number }[] {
|
||||
const out: { cells: Cell[]; index: number }[] = [];
|
||||
let lastName: Cell = null;
|
||||
for (let i = header + 1; i < grid.length; i++) {
|
||||
const row = grid[i] ?? [];
|
||||
if (row[1] === null || row[1] === undefined || row[1] === '') break;
|
||||
const name: Cell = row[0] ?? lastName;
|
||||
lastName = name;
|
||||
out.push({ cells: [name, ...row.slice(1)], index: i });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function toIp(entry: { cells: Cell[]; index: number }): { pier: string; ip: IpPoint } {
|
||||
const c = entry.cells;
|
||||
return {
|
||||
pier: String(c[0]),
|
||||
ip: { name: String(c[1]), x: Number(c[2]), y: Number(c[3]), angleDeg: Number(c[4] ?? 0) },
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user