feat(parse): report per-sheet recognition and block error sheets
- parseWorkbook returns a report per sheet (table counts, issues, ok) - sheets with E-level issues cannot be selected (spec section 2.3) - pier input carries its source sheet name Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
35d89dc4e2
commit
01d2bf4958
8 files changed
+122
-16
No files matched your search
+19
-3
@@ -118,6 +118,7 @@ export interface PierModel {
|
||||
columnHeight: number;
|
||||
soilCover: number;
|
||||
pileCount: number;
|
||||
pileLayout: string | null;
|
||||
pileTipEl: number | null;
|
||||
blockHeightFront: number;
|
||||
blockHeightBack: number;
|
||||
@@ -290,6 +291,7 @@ export function buildPier(input: PierInput): PierModel {
|
||||
columnHeight: levelBottom - columns[0].bottom,
|
||||
soilCover: input.groundEl - (input.foundationType === '현장타설말뚝' ? input.footingBottomEl : footingTop),
|
||||
pileCount: piles.length,
|
||||
pileLayout: input.foundationType === '말뚝기초' ? pileLayoutText() : null,
|
||||
pileTipEl: piles.length ? pileTip : null,
|
||||
blockHeightFront,
|
||||
blockHeightBack,
|
||||
@@ -313,12 +315,26 @@ function segmentTop(segments: Segment[], s: number): number {
|
||||
* Piles under one footing (spec §4.6): rows across the footing width, piles along the length,
|
||||
* both with an end distance of 0.7 m and an even spacing that never exceeds 2.25 m.
|
||||
*/
|
||||
export function pilesFor(s: number, topEl: number, length: number): PileModel[] {
|
||||
export function pileGrid(): { countS: number; countN: number; spacingS: number; spacingN: number } {
|
||||
const span = (size: number) => Math.floor((size - 2 * DIM.pileEnd) / DIM.pileSpacing) + 1;
|
||||
const countS = span(DIM.footingL);
|
||||
const countN = span(DIM.footingW);
|
||||
const spacingS = (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1);
|
||||
const spacingN = (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1);
|
||||
return {
|
||||
countS,
|
||||
countN,
|
||||
spacingS: (DIM.footingL - 2 * DIM.pileEnd) / (countS - 1),
|
||||
spacingN: (DIM.footingW - 2 * DIM.pileEnd) / (countN - 1),
|
||||
};
|
||||
}
|
||||
|
||||
/** Display string such as "4@2250 × 3@2250 = 20본" (gaps along the length × gaps across the width). */
|
||||
export function pileLayoutText(): string {
|
||||
const g = pileGrid();
|
||||
return `${g.countS - 1}@${Math.round(g.spacingS * 1000)} × ${g.countN - 1}@${Math.round(g.spacingN * 1000)} = ${g.countS * g.countN}본`;
|
||||
}
|
||||
|
||||
export function pilesFor(s: number, topEl: number, length: number): PileModel[] {
|
||||
const { countS, countN, spacingS, spacingN } = pileGrid();
|
||||
const out: PileModel[] = [];
|
||||
for (let i = 0; i < countS; i++) {
|
||||
for (let j = 0; j < countN; j++) {
|
||||
|
||||
@@ -5,24 +5,50 @@ import type { Bearing, FoundationType, Issue, IpPoint, PierInput, PierType, Row
|
||||
type Cell = string | number | null | undefined;
|
||||
type Grid = Cell[][];
|
||||
|
||||
export interface SheetReport {
|
||||
name: string;
|
||||
/** Pier input tables found (교각 정보 표 개수). */
|
||||
tables: { pier: number; ip: number; bearing: number };
|
||||
piers: number;
|
||||
issues: Issue[];
|
||||
/** A sheet with any E-level issue cannot be selected (spec §2.3). */
|
||||
ok: boolean;
|
||||
}
|
||||
|
||||
export interface ParseResult {
|
||||
piers: PierInput[];
|
||||
issues: Issue[];
|
||||
sheets: SheetReport[];
|
||||
}
|
||||
|
||||
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). */
|
||||
/** Reads every sheet, finds the pier tables by their header strings, and reports each sheet separately. */
|
||||
export function parseWorkbook(wb: XLSX.WorkBook): ParseResult {
|
||||
const piers: PierInput[] = [];
|
||||
const issues: Issue[] = [];
|
||||
const sheets: SheetReport[] = [];
|
||||
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));
|
||||
const sheetIssues: Issue[] = [];
|
||||
const found = parseSheet(grid, sheetName, sheetIssues);
|
||||
piers.push(...found);
|
||||
issues.push(...sheetIssues);
|
||||
sheets.push({
|
||||
name: sheetName,
|
||||
tables: {
|
||||
pier: findHeaders(grid, (r) => r[0] === '교각명' && r[1] === '교각형식').length,
|
||||
ip: findHeaders(grid, (r) => r[0] === '교각명' && r[1] === 'IP점').length,
|
||||
bearing: findHeaders(grid, (r) => r[0] === '교각명' && r[1] === '받침번호').length,
|
||||
},
|
||||
piers: found.length,
|
||||
issues: sheetIssues,
|
||||
ok: found.length > 0 && !sheetIssues.some((i) => i.level === 'E'),
|
||||
});
|
||||
}
|
||||
return { piers, issues };
|
||||
return { piers, issues, sheets };
|
||||
}
|
||||
|
||||
function parseSheet(grid: Grid, sheet: string, issues: Issue[]): PierInput[] {
|
||||
@@ -61,18 +87,19 @@ function parseSheet(grid: Grid, sheet: string, issues: Issue[]): PierInput[] {
|
||||
code: 'E03',
|
||||
level: 'E',
|
||||
pier: name,
|
||||
message: `${sheet} ${bad.row}행: 방위각 문자열을 읽을 수 없습니다. 예: 75°56'55.9"`,
|
||||
message: `${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}행: 교각형식 또는 기초 형식을 알 수 없습니다.` });
|
||||
issues.push({ code: 'E01', level: 'E', pier: name, message: `${index + 1}행: 교각형식 또는 기초 형식을 알 수 없습니다.` });
|
||||
continue;
|
||||
}
|
||||
piers.push({
|
||||
name,
|
||||
sheet,
|
||||
type,
|
||||
copingTopEl: Number(cells[2]),
|
||||
footingBottomEl: Number(cells[3]),
|
||||
|
||||
@@ -21,6 +21,8 @@ export interface Bearing {
|
||||
|
||||
export interface PierInput {
|
||||
name: string;
|
||||
/** Source sheet name in the workbook (1 sheet = 1 pier in the spec, several allowed). */
|
||||
sheet: string;
|
||||
type: PierType;
|
||||
copingTopEl: number;
|
||||
footingBottomEl: number;
|
||||
|
||||
+39
-8
@@ -2,7 +2,7 @@ import './style.css';
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { parseWorkbook } from './core/parseWorkbook';
|
||||
import { parseWorkbook, type SheetReport } from './core/parseWorkbook';
|
||||
import { buildPier, type PierModel } from './core/geometry';
|
||||
import { NOT_IMPLEMENTED_RULES, validateInput, validatePier } from './core/validate';
|
||||
import type { Issue, PierInput } from './core/types';
|
||||
@@ -16,17 +16,19 @@ interface Loaded {
|
||||
|
||||
const $ = <T extends HTMLElement>(id: string): T => document.getElementById(id) as T;
|
||||
|
||||
const VIEW_LABELS: Record<ViewKind, string> = { iso: '등각', front: '정면', side: '측면', top: '평면' };
|
||||
const VIEW_LABELS: Record<ViewKind, string> = { iso: '아이소', front: '정면', side: '측면', top: '평면' };
|
||||
|
||||
const state: {
|
||||
piers: Loaded[];
|
||||
parseIssues: Issue[];
|
||||
sheets: SheetReport[];
|
||||
current: { loaded: Loaded; scene: PierScene } | null;
|
||||
view: ViewKind;
|
||||
visible: Record<PartName, boolean>;
|
||||
} = {
|
||||
piers: [],
|
||||
parseIssues: [],
|
||||
sheets: [],
|
||||
current: null,
|
||||
view: 'iso',
|
||||
visible: Object.fromEntries(Object.keys(PART_LABELS).map((k) => [k, true])) as Record<PartName, boolean>,
|
||||
@@ -113,7 +115,7 @@ function setView(kind: ViewKind): void {
|
||||
|
||||
// --- Loading ----------------------------------------------------------------------
|
||||
function loadWorkbookBytes(bytes: Uint8Array): void {
|
||||
const { piers, issues: parseIssues } = parseWorkbook(XLSX.read(bytes, { type: 'array' }));
|
||||
const { piers, issues: parseIssues, sheets } = parseWorkbook(XLSX.read(bytes, { type: 'array' }));
|
||||
const loaded: Loaded[] = piers.map((input) => {
|
||||
const issues: Issue[] = [];
|
||||
let model: PierModel | null = null;
|
||||
@@ -129,17 +131,20 @@ function loadWorkbookBytes(bytes: Uint8Array): void {
|
||||
});
|
||||
state.piers = loaded;
|
||||
state.parseIssues = parseIssues;
|
||||
state.sheets = sheets;
|
||||
renderSheets();
|
||||
|
||||
// Only piers from sheets without E-level issues can be selected (spec §2.3).
|
||||
const selectable = (l: Loaded) => !!l.model && sheets.find((s) => s.name === l.input.sheet)?.ok === true;
|
||||
const select = $<HTMLSelectElement>('pier');
|
||||
select.replaceChildren();
|
||||
for (const item of loaded) {
|
||||
if (!item.model) continue;
|
||||
for (const item of loaded.filter(selectable)) {
|
||||
select.appendChild(new Option(item.input.name, item.input.name));
|
||||
}
|
||||
select.disabled = select.options.length === 0;
|
||||
const built = loaded.filter((l) => l.model).length;
|
||||
$('status').textContent = `교각 ${piers.length}기 인식, 형상 생성 ${built}기`;
|
||||
const first = loaded.find((l) => l.model);
|
||||
const built = loaded.filter(selectable).length;
|
||||
$('status').textContent = `시트 ${sheets.length}장 · 교각 ${piers.length}기 인식 · 선택 가능 ${built}기`;
|
||||
const first = loaded.find(selectable);
|
||||
if (first) showPier(first.input.name);
|
||||
else showPier(null);
|
||||
}
|
||||
@@ -192,12 +197,15 @@ function renderResults(loaded: Loaded | null): void {
|
||||
row(body, '기둥 중심 (국토좌표 m)', pt(m.pivot));
|
||||
row(body, '코핑 상면 EL 기준 (m)', fmt(loaded.input.copingTopEl));
|
||||
row(body, '전열 구간 상면 EL (m)', m.segments.map((s) => fmt(s.top)).join(' / '));
|
||||
row(body, '계단 경계 (BP1 기준, m)', m.segments.slice(1).map((s) => fmt(s.s0)).join(' / '));
|
||||
row(body, '횡방향 단차', '없음');
|
||||
row(body, '코핑 끝단 하면 EL (m)', fmt(m.endBottom));
|
||||
row(body, '코핑 Level 하면 EL (m)', fmt(m.levelBottom));
|
||||
row(body, '기둥 높이 (m)', fmt(m.derived.columnHeight));
|
||||
row(body, '확대기초 상면 EL (m)', m.footings.length ? fmt(m.footings[0].top) : '없음');
|
||||
row(body, '흙덮개 (m)', fmt(m.derived.soilCover));
|
||||
row(body, '말뚝 수 (본)', String(m.derived.pileCount));
|
||||
row(body, '말뚝 배치', m.derived.pileLayout ?? '없음');
|
||||
row(body, '말뚝 선단 EL (m)', m.derived.pileTipEl === null ? '없음' : fmt(m.derived.pileTipEl));
|
||||
row(body, '받침블럭 콘크리트 전열 / 후열 (mm)', `${Math.round(m.derived.blockHeightFront * 1000)} / ${Math.round(m.derived.blockHeightBack * 1000)}`);
|
||||
} else {
|
||||
@@ -226,6 +234,28 @@ function renderResults(loaded: Loaded | null): void {
|
||||
for (const text of items) defaults.appendChild(el('li', text));
|
||||
}
|
||||
|
||||
function renderSheets(): void {
|
||||
const box = $<HTMLUListElement>('sheets');
|
||||
box.replaceChildren();
|
||||
if (state.sheets.length === 0) {
|
||||
box.appendChild(el('li', '엑셀을 불러오면 시트별 인식 결과가 표시됩니다.'));
|
||||
return;
|
||||
}
|
||||
for (const s of state.sheets) {
|
||||
const li = el('li');
|
||||
const status = s.ok ? '선택 가능' : '선택 불가 (오류)';
|
||||
const head = el('span', `${s.name} · ${status}`, s.ok ? 'sheet-ok' : 'code');
|
||||
const detail = document.createTextNode(` · 교각 ${s.tables.pier}표 · IP ${s.tables.ip}표 · 받침 ${s.tables.bearing}표`);
|
||||
li.append(head, detail);
|
||||
box.appendChild(li);
|
||||
for (const issue of s.issues) {
|
||||
const sub = el('li', `${issue.code} ${issue.pier ? `[${issue.pier}] ` : ''}${issue.message}`, issue.level === 'E' ? 'code' : undefined);
|
||||
sub.style.marginLeft = '16px';
|
||||
box.appendChild(sub);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Events -----------------------------------------------------------------------
|
||||
$<HTMLButtonElement>('sample').addEventListener('click', async () => {
|
||||
$('status').textContent = '샘플 불러오는 중…';
|
||||
@@ -252,6 +282,7 @@ $<HTMLSelectElement>('pier').addEventListener('change', (ev) => showPier((ev.tar
|
||||
|
||||
buildPartToggles();
|
||||
buildViewButtons();
|
||||
renderSheets();
|
||||
renderResults(null);
|
||||
|
||||
// Deep link for the bundled sample: open with ?sample=1 to load P1 without clicking.
|
||||
|
||||
@@ -216,6 +216,11 @@ td:first-child {
|
||||
color: var(--warn);
|
||||
}
|
||||
|
||||
.issues .sheet-ok {
|
||||
font-weight: 600;
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
Reference in new issue
Block a user