feat(abut): 교대 형상·입력 패널과 교각 받침·솔플레이트 개편
- 교대 형상(src/core/abut, 3D 장면, 목록·카메라·맞춤)과 교대 입력 패널 추가 - 교각 받침 기준 카드 제거, 솔플레이트 규칙을 교각별 입력으로 변경 - 받침 ELEVATION·솔플레이트 TABLE을 모델값으로 연결 - 교대 입력값 카드: 형식 행에 시점·종점 표시, 제목을 "A1 입력값 [입력]"으로 변경 - 테스트 614건 통과, 타입 검사 통과 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b1b2ad1ddb
commit
1d31d2f3a9
44 files changed
+2571
-120
No files matched your search
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 교대 입력 읽기 (2026-11-03, spec_abut §2): 입력 엑셀의 교대 행을 읽는다. 교대 표는 교각 표와 같은 시트에 있으며,
|
||||
* 첫 두 헤더 칸(교대명 + 형식 / IP점 / 받침번호)으로 구분한다. 워크북 객체에 대해 순수하다. 좌표: 시트 x_m = 북, y_m = 동 → 내부 (E, N) = (y_m, x_m).
|
||||
*
|
||||
* 받침높이 (2026-11-03 결정 1): 교각과 같은 정의 — 패드 상면(z)에서 솔플레이트 상면까지.
|
||||
* 받침 행의 값을 그대로 읽는다(A1 시트 값 275). 여기서 계산하지 않는다.
|
||||
*/
|
||||
import * as XLSX from 'xlsx';
|
||||
import { parseDms } from '../dms';
|
||||
import type { Issue } from '../types';
|
||||
import type { AbutInput, AbutPoint, AbutType, WingForm } from './geometry';
|
||||
import type { AbutShape } from './shape';
|
||||
|
||||
type Cell = string | number | null | undefined;
|
||||
type Grid = Cell[][];
|
||||
|
||||
export interface AbutRecordBearing {
|
||||
no: number;
|
||||
/** Position (E, N), m: E = y_m, N = x_m. */
|
||||
pos: AbutPoint;
|
||||
z: number;
|
||||
/** 교좌면_EL of this bearing (m). Absent = the block rule (z − 블럭 높이). */
|
||||
seatEl?: number;
|
||||
/** 이 받침에 적힌 받침높이 (mm). */
|
||||
bearingHeightMm: number;
|
||||
row: string;
|
||||
}
|
||||
|
||||
export interface AbutRecord {
|
||||
name: string;
|
||||
sheet: string;
|
||||
type: AbutType;
|
||||
/** 첫 받침(구간 ①)의 교좌면 EL (m). */
|
||||
seatEl: number;
|
||||
/** 기초 하단 EL (m), 지반고 EL (m), 기초 형식, 말뚝길이 (mm). */
|
||||
foundationBottomEl: number;
|
||||
groundEl: number;
|
||||
foundation: string;
|
||||
pileLengthMm: number;
|
||||
/** skew (deg) from the DMS text, and 종단경사 i (decimal). */
|
||||
skewDeg: number;
|
||||
slopeI: number;
|
||||
ip1: AbutPoint;
|
||||
ip2: AbutPoint;
|
||||
ip3: AbutPoint;
|
||||
bearings: AbutRecordBearing[];
|
||||
/** 받침 행에서 읽은 받침높이 (mm). 모든 받침이 같아야 하며, 다르면 AP07 경고. */
|
||||
bearingHeightMm: number;
|
||||
/** 측별 날개벽 형식 (편집 카드에서 정함). Absent = 양측 일반형. */
|
||||
wings?: { left: WingForm; right: WingForm };
|
||||
/** 형상 입력 (벽체·흉벽, 확대기초·말뚝, mm). Only the changed values; the rest are the defaults. Absent = all defaults. */
|
||||
shape?: Partial<AbutShape>;
|
||||
}
|
||||
|
||||
export interface AbutParseResult {
|
||||
abutments: AbutRecord[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
const num = (c: Cell) => (c === null || c === undefined || c === '' ? NaN : Number(c));
|
||||
const isBlank = (c: Cell) => c === null || c === undefined || c === '';
|
||||
|
||||
/** 교대 표의 헤더 행 (첫 두 칸). */
|
||||
const isAbutInfoHeader = (r: Cell[]) => r[0] === '교대명' && r[1] === '형식';
|
||||
const isAbutIpHeader = (r: Cell[]) => r[0] === '교대명' && r[1] === 'IP점';
|
||||
const isAbutBearingHeader = (r: Cell[]) => r[0] === '교대명' && r[1] === '받침번호';
|
||||
|
||||
/** 헤더 아래 데이터 행, 둘째 칸이 빌 때까지. 교대명 칸이 비면 위 행을 이어받는다. */
|
||||
function readBlock(grid: Grid, header: number): { cells: Cell[]; row: number }[] {
|
||||
const out: { cells: Cell[]; row: number }[] = [];
|
||||
let last: Cell = null;
|
||||
for (let i = header + 1; i < grid.length; i++) {
|
||||
const r = grid[i] ?? [];
|
||||
if (isBlank(r[1])) break;
|
||||
const name: Cell = isBlank(r[0]) ? last : r[0];
|
||||
last = name;
|
||||
out.push({ cells: [name, ...r.slice(1)], row: i + 1 });
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export function parseAbutments(wb: XLSX.WorkBook): AbutParseResult {
|
||||
const issues: Issue[] = [];
|
||||
const info: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
const ips: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
const bearings: { cells: Cell[]; row: number; sheet: string }[] = [];
|
||||
|
||||
for (const sheet of wb.SheetNames) {
|
||||
const grid = XLSX.utils.sheet_to_json<Cell[]>(wb.Sheets[sheet], { header: 1, raw: true, defval: null }) as Grid;
|
||||
grid.forEach((r, i) => {
|
||||
const pick = (match: (r: Cell[]) => boolean, into: typeof info) => {
|
||||
if (!match(r)) return;
|
||||
for (const rec of readBlock(grid, i)) into.push({ ...rec, sheet });
|
||||
};
|
||||
pick(isAbutInfoHeader, info);
|
||||
pick(isAbutIpHeader, ips);
|
||||
pick(isAbutBearingHeader, bearings);
|
||||
});
|
||||
}
|
||||
|
||||
const abutments: AbutRecord[] = [];
|
||||
for (const rec of info) {
|
||||
const c = rec.cells;
|
||||
const name = String(c[0] ?? '').trim();
|
||||
const where = `${rec.sheet} ${rec.row}행`;
|
||||
if (!/^A\d+$/.test(name)) {
|
||||
issues.push({ code: 'AP01', level: 'E', sheet: rec.sheet, message: `${where}: 교대명 "${String(c[0] ?? '')}"이 올바르지 않습니다 (A1, A2 …).` });
|
||||
continue;
|
||||
}
|
||||
const type = String(c[1] ?? '').trim();
|
||||
if (type !== 'A1' && type !== 'A2') {
|
||||
issues.push({ code: 'AP02', level: 'E', pier: name, sheet: rec.sheet, message: `${where}: 교대 ${name}의 형식 "${type}"은 A1 또는 A2여야 합니다.` });
|
||||
continue;
|
||||
}
|
||||
const skewDeg = parseDms(String(c[7] ?? ''));
|
||||
if (skewDeg === null) {
|
||||
issues.push({ code: 'AP03', level: 'E', pier: name, sheet: rec.sheet, message: `${where}: 교대 ${name}의 skew "${String(c[7] ?? '')}"를 읽지 못했습니다.` });
|
||||
continue;
|
||||
}
|
||||
abutments.push({
|
||||
name,
|
||||
sheet: rec.sheet,
|
||||
type,
|
||||
seatEl: num(c[2]),
|
||||
foundationBottomEl: num(c[3]),
|
||||
groundEl: num(c[4]),
|
||||
foundation: String(c[5] ?? ''),
|
||||
pileLengthMm: num(c[6]),
|
||||
skewDeg,
|
||||
slopeI: num(c[8]),
|
||||
ip1: { e: NaN, n: NaN },
|
||||
ip2: { e: NaN, n: NaN },
|
||||
ip3: { e: NaN, n: NaN },
|
||||
bearings: [],
|
||||
bearingHeightMm: NaN,
|
||||
});
|
||||
}
|
||||
|
||||
for (const rec of ips) {
|
||||
const name = String(rec.cells[0] ?? '').trim();
|
||||
const ab = abutments.find((a) => a.name === name);
|
||||
if (!ab) {
|
||||
issues.push({ code: 'AP04', level: 'E', sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
const point = { e: num(rec.cells[3]), n: num(rec.cells[2]) };
|
||||
const ipName = String(rec.cells[1] ?? '');
|
||||
if (ipName === 'IP1') ab.ip1 = point;
|
||||
else if (ipName === 'IP2') ab.ip2 = point;
|
||||
else if (ipName === 'IP3') ab.ip3 = point;
|
||||
}
|
||||
|
||||
for (const rec of bearings) {
|
||||
const c = rec.cells;
|
||||
const name = String(c[0] ?? '').trim();
|
||||
const ab = abutments.find((a) => a.name === name);
|
||||
if (!ab) {
|
||||
issues.push({ code: 'AP04', level: 'E', sheet: rec.sheet, message: `${rec.sheet} ${rec.row}행: 교대명 ${name}의 교대 정보 행이 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
const h = num(c[8]);
|
||||
ab.bearings.push({
|
||||
no: num(c[1]),
|
||||
pos: { e: num(c[3]), n: num(c[2]) },
|
||||
z: num(c[4]),
|
||||
seatEl: isBlank(c[7]) ? undefined : num(c[7]),
|
||||
bearingHeightMm: h,
|
||||
row: `${rec.sheet} ${rec.row}행`,
|
||||
});
|
||||
}
|
||||
|
||||
for (const ab of abutments) {
|
||||
const missing = [ab.ip1, ab.ip2, ab.ip3].some((p) => !Number.isFinite(p.e) || !Number.isFinite(p.n));
|
||||
if (missing) issues.push({ code: 'AP05', level: 'E', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: IP1·IP2·IP3 중 빠진 점이 있습니다.` });
|
||||
if (ab.bearings.length < 2) issues.push({ code: 'AP06', level: 'E', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: 받침이 2개 이상 필요합니다.` });
|
||||
const heights = [...new Set(ab.bearings.map((b) => b.bearingHeightMm))];
|
||||
if (heights.length > 1) issues.push({ code: 'AP07', level: 'W', pier: ab.name, sheet: ab.sheet, message: `교대 ${ab.name}: 받침높이가 받침마다 다릅니다 (${heights.join(', ')} mm). 첫 값을 씁니다.` });
|
||||
ab.bearingHeightMm = heights[0] ?? NaN;
|
||||
}
|
||||
|
||||
return { abutments: abutments.filter((a) => Number.isFinite(a.ip1.e) && a.bearings.length >= 2), issues };
|
||||
}
|
||||
|
||||
/** The layout input of AA1–AA2 from a parsed 교대 record. */
|
||||
export function toAbutInput(rec: AbutRecord): AbutInput {
|
||||
return {
|
||||
name: rec.name,
|
||||
type: rec.type,
|
||||
seatEl: rec.seatEl,
|
||||
ip1: rec.ip1,
|
||||
ip2: rec.ip2,
|
||||
ip3: rec.ip3,
|
||||
bearings: rec.bearings.map((b) => ({ no: b.no, pos: b.pos, z: b.z, seatEl: b.seatEl })),
|
||||
skewDeg: rec.skewDeg,
|
||||
slopeI: rec.slopeI,
|
||||
wings: rec.wings,
|
||||
foundationBottomEl: rec.foundationBottomEl,
|
||||
pileLengthMm: rec.pileLengthMm,
|
||||
foundationType: rec.foundation,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user