feat(bearing): 솔플레이트 규칙을 교각별 입력으로 바꾸고 도면 불러오기 제거 (C안)
- 받침 기준 카드, 도면 불러오기(xlsx·csv·dxf), 도면 참조 표·오버레이 제거 - 교각마다 솔플레이트 규칙 입력: 받침높이 H(솔플레이트 포함) 211, 교직 L 1000, 교축 W 1100, 종단 기울기 S −0.5 % (기본값, 가정 표시) - 중심 두께 T = ⌈22 + L·|S|/200⌉ 로 계산해 얇은 쪽 T2가 22 mm 이상이 되게 함 (기본값 T 25, T2 22.5, h3 = H − T = 186) - geometry·scene·SVG는 교각 규칙에서 만든 같은 솔플레이트 계획을 모든 받침에 사용 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f25e36c050
commit
36b26209df
23 files changed
+257
-1081
No files matched your search
@@ -1,148 +0,0 @@
|
||||
import type { PierInput } from './types';
|
||||
import type { PierModel } from './geometry';
|
||||
|
||||
/**
|
||||
* 교량받침 reference tables from the P1 상세도 (도면값, 읽기 전용, 2026-10-23). They are not used in the model:
|
||||
* the model keeps the imported values (Q16). EL.X is the drawing's bearing level; E = h1 + h2 + h3 + h4.
|
||||
*/
|
||||
export interface BearingRefRow {
|
||||
no: number;
|
||||
row: '전열' | '후열';
|
||||
/** EL.X (m). */
|
||||
elx: number;
|
||||
/** 블럭 높이 h1 (mm, 도면). */
|
||||
h1: number;
|
||||
/** 모르타르 h2 (mm). */
|
||||
h2: number;
|
||||
/** 받침 h3 (mm). */
|
||||
h3: number;
|
||||
/** 솔플레이트 h4 (mm). */
|
||||
h4: number;
|
||||
/** E (mm, 도면). */
|
||||
E: number;
|
||||
A: number;
|
||||
B: number;
|
||||
C1: number;
|
||||
C2: number | null;
|
||||
C3: number | null;
|
||||
C4: number | null;
|
||||
C5: number | null;
|
||||
C6: number | null;
|
||||
S1: number;
|
||||
}
|
||||
|
||||
export interface SolplateRefRow {
|
||||
no: number;
|
||||
/** 기울기 S (%), negative as drawn. */
|
||||
s: number;
|
||||
L: number;
|
||||
W: number;
|
||||
/** 중심 두께 T (mm). The solplate tapers along the bridge axis. */
|
||||
T: number;
|
||||
/** 두께 at the 교각축 직각 (−) side (mm). */
|
||||
T1: number;
|
||||
/** 두께 at the 교각축 직각 (+) side (mm). */
|
||||
T2: number;
|
||||
}
|
||||
|
||||
const FRONT = { h1: 164, h2: 50, h3: 181, h4: 30, E: 425 };
|
||||
const BACK = { h1: 139, h2: 50, h3: 181, h4: 30, E: 400 };
|
||||
|
||||
export const BEARING_ELEVATION_REF: BearingRefRow[] = [
|
||||
{ no: 1, row: '전열', elx: 43.625, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 },
|
||||
{ no: 2, row: '전열', elx: 43.697, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 },
|
||||
{ no: 3, row: '전열', elx: 43.768, ...FRONT, A: 700, B: 790, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 },
|
||||
{ no: 4, row: '전열', elx: 43.840, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 },
|
||||
{ no: 5, row: '전열', elx: 43.912, ...FRONT, A: 700, B: 640, C1: 600, C2: 1298, C3: 2500, C4: 600, C5: 600, C6: 1200, S1: 852 },
|
||||
{ no: 6, row: '후열', elx: 43.600, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 },
|
||||
{ no: 7, row: '후열', elx: 43.672, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 },
|
||||
{ no: 8, row: '후열', elx: 43.744, ...BACK, A: 700, B: 650, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 },
|
||||
{ no: 9, row: '후열', elx: 43.816, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 },
|
||||
{ no: 10, row: '후열', elx: 43.887, ...BACK, A: 700, B: 540, C1: 602, C2: null, C3: null, C4: null, C5: null, C6: null, S1: 950 },
|
||||
];
|
||||
|
||||
export const SOLPLATE_REF: SolplateRefRow[] = [
|
||||
{ no: 1, s: -1.910, L: 800, W: 740, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 2, s: -1.907, L: 800, W: 740, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 3, s: -1.905, L: 800, W: 890, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 4, s: -1.903, L: 800, W: 740, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 5, s: -1.901, L: 800, W: 740, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 6, s: -1.910, L: 800, W: 640, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 7, s: -1.907, L: 800, W: 640, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 8, s: -1.905, L: 800, W: 750, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 9, s: -1.903, L: 800, W: 640, T: 30, T1: 38, T2: 22 },
|
||||
{ no: 10, s: -1.901, L: 800, W: 640, T: 30, T1: 38, T2: 22 },
|
||||
];
|
||||
|
||||
/** 중심 두께 T of the 공통 규칙 (30 mm). T1 = T + L·|S|/2 (−측), T2 = T − L·|S|/2 (+측), so T1 + T2 = 2T. */
|
||||
export const SOLPLATE_T_RULE = 30;
|
||||
|
||||
/** T1 / T2 by the drawing's rule: T ± L·|S|/2, rounded to mm (S in %). */
|
||||
export function solplateTs(T: number, L: number, s: number): { T1: number; T2: number } {
|
||||
const half = (L * Math.abs(s)) / 200;
|
||||
return { T1: Math.round(T + half), T2: Math.round(T - half) };
|
||||
}
|
||||
|
||||
/** 공통 규칙 (2단계, 승인 2026-10-23): inputs that drive E, L, W, T1, T2. Defaults are the drawing's values. */
|
||||
export interface BearingRules {
|
||||
/** 모르타르 h2 (mm). */
|
||||
h2: number;
|
||||
/** 받침 높이 h3 (mm). */
|
||||
h3: number;
|
||||
/** 솔플레이트 중심 두께 T (mm). */
|
||||
T: number;
|
||||
/** 솔플 여유 (mm): L = A + margin, W = B + margin. */
|
||||
margin: number;
|
||||
}
|
||||
|
||||
export const BEARING_RULES_DEFAULT: BearingRules = { h2: 50, h3: 181, T: SOLPLATE_T_RULE, margin: 100 };
|
||||
|
||||
/** Values of one bearing with the 공통 규칙 applied: E = h1 + h2 + h3 + h4, L = A + margin, W = B + margin, T1/T2 from T. */
|
||||
export function applyBearingRules(row: BearingRefRow, rules: BearingRules): { E: number; L: number; W: number; T1: number; T2: number } {
|
||||
const sp = SOLPLATE_REF[row.no - 1];
|
||||
const L = row.A + rules.margin;
|
||||
const { T1, T2 } = solplateTs(rules.T, L, sp.s);
|
||||
return { E: row.h1 + rules.h2 + rules.h3 + row.h4, L, W: row.B + rules.margin, T1, T2 };
|
||||
}
|
||||
|
||||
/** 받침 기준 (Q16, 승인 2026-10-23): 'excel' = 엑셀 z (기본), 'drawing' = 도면 EL.X. Project-wide. */
|
||||
export type BearingBasis = 'excel' | 'drawing';
|
||||
|
||||
/** One bearing's drawing values entered on ② (mm for h1, h2; m for EL.X). */
|
||||
export interface DrawingBearing {
|
||||
no: number;
|
||||
elx: number;
|
||||
h1: number;
|
||||
h2: number;
|
||||
/** 받침 평면 A, B (mm) and solplate slope S (%), when read from the drawing (승인 2026-10-23). */
|
||||
A?: number;
|
||||
B?: number;
|
||||
S?: number;
|
||||
}
|
||||
|
||||
/** Tolerance for the coping top check against the drawing (승인 2026-10-23: 5 mm). */
|
||||
export const DRAWING_TOL_MM = 5;
|
||||
|
||||
/** The input with each bearing's z replaced by the drawing EL.X, for bearings that have a drawing value. */
|
||||
export function withDrawingBasis(input: PierInput, drawing: DrawingBearing[]): PierInput {
|
||||
const byNo = new Map(drawing.map((d) => [d.no, d]));
|
||||
return {
|
||||
...input,
|
||||
bearings: input.bearings.map((b) => {
|
||||
const d = byNo.get(b.no);
|
||||
return d ? { ...b, z: d.elx } : b;
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/** Coping top implied by each drawing row (EL.X − h1 − h2) against the model's coping top; differences in mm. */
|
||||
export function drawingCopingCheck(model: PierModel, drawing: DrawingBearing[]): { no: number; diffMm: number; ok: boolean }[] {
|
||||
const front = model.blocks.filter((b) => b.row === '전열');
|
||||
const back = model.blocks.filter((b) => b.row === '후열');
|
||||
return drawing.map((d) => {
|
||||
const implied = d.elx - (d.h1 + d.h2) / 1000;
|
||||
const modelTop = d.no <= 5 ? model.segments[d.no - 1]?.top : back[d.no - 6]?.bottom;
|
||||
const diffMm = modelTop === undefined ? NaN : Math.round((modelTop - implied) * 1000 * 10) / 10;
|
||||
return { no: d.no, diffMm, ok: Number.isFinite(diffMm) && Math.abs(diffMm) <= DRAWING_TOL_MM };
|
||||
});
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/**
|
||||
* 도면 불러오기 (승인 2026-10-23): reads the bearing drawing values from a sheet (xlsx or csv rows). Pure: the caller
|
||||
* passes the rows (arrays of cells). The header row names: No, EL.X (m), h1 (mm), h2 (mm). 열 and other columns are ignored.
|
||||
*/
|
||||
import type { DrawingBearing } from './bearing-ref';
|
||||
|
||||
export interface DrawingImportResult {
|
||||
rows: DrawingBearing[];
|
||||
errors: string[];
|
||||
}
|
||||
|
||||
// Header names ignore case, spaces, dots and a unit in parentheses: 'EL. X (m)' → 'elx'.
|
||||
const norm = (v: unknown) => String(v ?? '').replace(/\(.*?\)/g, '').toLowerCase().replace(/[\s.]/g, '');
|
||||
|
||||
/** Column of each header name, or -1 when missing. */
|
||||
function headerIndex(header: unknown[]): { no: number; elx: number; h1: number; h2: number } {
|
||||
const find = (...names: string[]) => header.findIndex((c) => names.includes(norm(c)));
|
||||
return { no: find('no'), elx: find('elx', 'el'), h1: find('h1', '블럭높이h1'), h2: find('h2', '몰탈h2', '모르타르h2') };
|
||||
}
|
||||
|
||||
export function parseDrawingRows(rows: unknown[][]): DrawingImportResult {
|
||||
const errors: string[] = [];
|
||||
const headerAt = rows.findIndex((r) => {
|
||||
const idx = headerIndex(r);
|
||||
return idx.no >= 0 && idx.elx >= 0 && idx.h1 >= 0 && idx.h2 >= 0;
|
||||
});
|
||||
if (headerAt < 0) {
|
||||
return { rows: [], errors: ['머리글을 찾지 못했습니다. No, EL.X, h1, h2 열이 있어야 합니다.'] };
|
||||
}
|
||||
const idx = headerIndex(rows[headerAt]);
|
||||
const out: DrawingBearing[] = [];
|
||||
const seen = new Set<number>();
|
||||
rows.slice(headerAt + 1).forEach((r, i) => {
|
||||
const rowNo = headerAt + 2 + i;
|
||||
if (r.every((c) => String(c ?? '').trim() === '')) return;
|
||||
const no = Number(r[idx.no]);
|
||||
const elx = Number(r[idx.elx]);
|
||||
const h1 = Number(r[idx.h1]);
|
||||
const h2 = Number(r[idx.h2]);
|
||||
if (!Number.isInteger(no) || no < 1) {
|
||||
errors.push(`${rowNo}행: No가 올바르지 않습니다.`);
|
||||
return;
|
||||
}
|
||||
if (seen.has(no)) {
|
||||
errors.push(`${rowNo}행: No ${no}가 중복됩니다.`);
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(elx) || elx < 30 || elx > 100) {
|
||||
errors.push(`${rowNo}행: EL.X는 m 단위로 적어 주세요 (예: 43.625). 값: ${String(r[idx.elx])}`);
|
||||
return;
|
||||
}
|
||||
if (!Number.isFinite(h1) || h1 <= 0 || h1 > 1000 || !Number.isFinite(h2) || h2 <= 0 || h2 > 1000) {
|
||||
errors.push(`${rowNo}행: h1·h2는 mm 단위 양수로 적어 주세요.`);
|
||||
return;
|
||||
}
|
||||
seen.add(no);
|
||||
out.push({ no, elx, h1, h2 });
|
||||
});
|
||||
if (out.length === 0) errors.push('불러온 행이 없습니다.');
|
||||
return { rows: out.sort((a, b) => a.no - b.no), errors };
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* 도면에서 받침 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[] = [];
|
||||
// The drawing has other "EL.X" labels (leader lines). The table header is the one with h1 and h2 on the same row.
|
||||
// The header is the EL.X label with the most table column names on its row, to its right (within 2500).
|
||||
const TABLE_NAMES = ['h1', 'h2', 'h3', 'h4', 'a', 'b', 'c1', 'c2', 'c3', 'c4', 'c5', 'c6', 's1', 's2'];
|
||||
const rowScore = (t: DxfText) =>
|
||||
texts.filter((u) => Math.abs(u.y - t.y) <= 60 && u.x > t.x && u.x - t.x <= 2500 && TABLE_NAMES.includes(norm(u.text))).length;
|
||||
const candidates = texts.filter((t) => norm(t.text) === 'elx');
|
||||
const best = candidates.map((t) => ({ t, score: rowScore(t) })).sort((a, b) => b.score - a.score)[0];
|
||||
if (!best || best.score < 2) return { rows: [], drawing: [], errors: ['EL.X 머리글(표의 열 이름과 같은 행)을 찾지 못했습니다.'] };
|
||||
const elHeader = best.t;
|
||||
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 }));
|
||||
// The No header is the 'NO' label on the table's header row (the drawing has other 'NO' labels).
|
||||
const noHeader = texts.find((t) => norm(t.text) === 'no' && Math.abs(t.y - headerY) <= 200);
|
||||
// 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, A: r.values.a, B: r.values.b });
|
||||
}
|
||||
return { rows, drawing: drawing.sort((a, b) => a.no - b.no), errors };
|
||||
}
|
||||
|
||||
export interface SolplateTableRow {
|
||||
no: number;
|
||||
/** 기울기 S (%), negative as drawn. */
|
||||
S: number;
|
||||
L: number;
|
||||
W: number;
|
||||
/** 중심 두께 T, and the 교각축 직각 (−) and (+) side thicknesses T1, T2 (mm). */
|
||||
T: number;
|
||||
T1: number;
|
||||
T2: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 솔플레이트 TABLE (승인 2026-10-23): the header row has NO, S(%), L, W, T, T1, T2; each bearing row gives the values under
|
||||
* their headers, as in the bearing ELEVATION table.
|
||||
*/
|
||||
export function parseSolplateTable(texts: DxfText[]): { rows: SolplateTableRow[]; errors: string[] } {
|
||||
// The drawing has more than one "S(%)" label: the header is the one with L, W, T, T1 and T2 on its row.
|
||||
const SOL_NAMES = ['l', 'w', 't', 't1', 't2'];
|
||||
const solScore = (t: DxfText) => texts.filter((u) => Math.abs(u.y - t.y) <= 60 && SOL_NAMES.includes(norm(u.text))).length;
|
||||
const sCandidates = texts.filter((t) => norm(t.text) === 's(%)' || norm(t.text) === 's%');
|
||||
const sBest = sCandidates.map((t) => ({ t, score: solScore(t) })).sort((a, b) => b.score - a.score)[0];
|
||||
if (!sBest || sBest.score < 5) return { rows: [], errors: ['솔플레이트 TABLE의 머리글(NO, L, W, T, T1, T2)이 모두 있어야 합니다.'] };
|
||||
const sHeader = sBest.t;
|
||||
const headerY = sHeader.y;
|
||||
const headerRow = texts.filter((t) => Math.abs(t.y - headerY) <= 60);
|
||||
const key = (name: string) => headerRow.find((t) => norm(t.text) === name);
|
||||
const cols = { S: key('s(%)') ?? key('s%'), L: key('l'), W: key('w'), T: key('t'), T1: key('t1'), T2: key('t2') };
|
||||
const noHeader = headerRow.find((t) => norm(t.text) === 'no');
|
||||
if (!cols.L || !cols.W || !cols.T || !cols.T1 || !cols.T2 || !noHeader) {
|
||||
return { rows: [], errors: ['솔플레이트 TABLE의 머리글(NO, L, W, T, T1, T2)이 모두 있어야 합니다.'] };
|
||||
}
|
||||
const noCells = texts.filter((t) => NUMBER.test(t.text) && Number.isInteger(Number(t.text)) && Math.abs(t.x - noHeader.x) <= 250 && t.y < headerY - 60);
|
||||
const at = (col: DxfText, y: number) => {
|
||||
const hit = texts.find((t) => Math.abs(t.x - col.x) <= 150 && Math.abs(t.y - y) <= 120 && NUMBER.test(t.text));
|
||||
return hit ? Number(hit.text) : NaN;
|
||||
};
|
||||
const rows: SolplateTableRow[] = [];
|
||||
const errors: string[] = [];
|
||||
for (const cell of noCells.sort((a, b) => b.y - a.y)) {
|
||||
const r = { no: Number(cell.text), S: cols.S ? at(cols.S, cell.y) : NaN, L: at(cols.L, cell.y), W: at(cols.W, cell.y), T: at(cols.T, cell.y), T1: at(cols.T1, cell.y), T2: at(cols.T2, cell.y) };
|
||||
if (Object.values(r).some((v) => Number.isNaN(v))) errors.push(`No ${r.no}: 솔플레이트 값이 빠졌습니다.`);
|
||||
else rows.push(r);
|
||||
}
|
||||
return { rows: rows.sort((a, b) => a.no - b.no), errors };
|
||||
}
|
||||
+8
-11
@@ -5,7 +5,8 @@ import { type CopingRebarSettings, DEFAULT_COPING_REBAR, DEFAULT_TOP_SPLICE, typ
|
||||
import { type ColumnRebarSettings, DEFAULT_COLUMN_REBAR } from './rebar/column-settings';
|
||||
import { type FootingRebarSettings, DEFAULT_FOOTING_REBAR } from './rebar/footing-settings';
|
||||
import { layoutText, resolveRowCounts, rowLayout, type PileGeometry } from './piles';
|
||||
import type { PierInput, Row } from './types';
|
||||
import type { PierInput, Row, SolplatePlan } from './types';
|
||||
import { SOLPLATE_RULE_DEFAULT, solplatePlan } from './solplate-rule';
|
||||
import { type CopingGeom, P1_COPING_GEOM } from './coping-geom';
|
||||
|
||||
/** Dimensions in m. Sources: intent.PR sheet (P1 rules) and spec.md §3 defaults. */
|
||||
@@ -74,8 +75,8 @@ export interface BlockModel {
|
||||
padTop: number;
|
||||
/** Skew (deg, −90…90] of the bearing row against the perpendicular of the stretch axis; the block sides follow the row. */
|
||||
rowSkewDeg: number;
|
||||
/** Solplate plan of this bearing (A, B, S, T, margin, h3), when the input gives it. */
|
||||
plan?: { A: number; B: number; S: number; T: number; margin: number; h3: number };
|
||||
/** Solplate of this bearing (C안, 2026-11-03): the pier's rule, the same for every bearing. */
|
||||
plan: SolplatePlan;
|
||||
}
|
||||
|
||||
/** Skew of a bearing row (deg) from the bearing azimuth and the stretch axis direction u (see {@link BlockModel.rowSkewDeg}). */
|
||||
@@ -432,19 +433,15 @@ export function buildPier(input: PierInput): PierModel {
|
||||
const blocks: BlockModel[] = [];
|
||||
const bearingAz = new Map(input.bearings.map((r) => [r.no, r.azimuthDeg]));
|
||||
const skewOf = (no: number, k: number) => rowSkewDeg(bearingAz.get(no) ?? NaN, axis.segments[k].u);
|
||||
// Solplate plans from the input (승인 2026-10-23); the 공통 규칙 default is T 30, margin 100, h3 181.
|
||||
const rules = input.solplate ?? { T: 30, margin: 100, h3: 181 };
|
||||
const planOf = (no: number) => {
|
||||
const p = input.bearingPlans?.find((q) => q.no === no);
|
||||
return p ? { A: p.A, B: p.B, S: p.S, T: rules.T, margin: rules.margin, h3: rules.h3 } : undefined;
|
||||
};
|
||||
// Solplate of every bearing from the pier's rule (C안, 2026-11-03): the same plan for each bearing.
|
||||
const plan = solplatePlan(input.solplate ?? SOLPLATE_RULE_DEFAULT);
|
||||
frontPlaced.forEach((b) => {
|
||||
const bottom = segmentTop(segments, b.g);
|
||||
blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan: planOf(b.no) });
|
||||
blocks.push({ no: b.no, row: '전열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan });
|
||||
});
|
||||
backPlaced.forEach((b) => {
|
||||
const bottom = b.z - h2;
|
||||
blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan: planOf(b.no) });
|
||||
blocks.push({ no: b.no, row: '후열', k: b.k, s: b.s, g: b.g, n: b.n, bottom, top: b.z - DIM.padThickness, padTop: b.z, rowSkewDeg: skewOf(b.no, b.k), plan });
|
||||
});
|
||||
|
||||
const blockHeightFront = blocks.find((b) => b.row === '전열')!.top - blocks.find((b) => b.row === '전열')!.bottom;
|
||||
|
||||
@@ -33,10 +33,6 @@ export interface ProjectFile {
|
||||
history: { version: string; note: string; at: string }[];
|
||||
/** ③ 공통 설정 (fck · fy). Absent in older files: the defaults are used. */
|
||||
common?: Material;
|
||||
/** 받침 기준 (project-wide): 'excel' (default) or 'drawing'. Absent = 'excel'. */
|
||||
bearingBasis?: 'excel' | 'drawing';
|
||||
/** 도면 받침 값 per pier name (EL.X in m, h1 and h2 in mm). */
|
||||
bearingDrawing?: Record<string, { no: number; elx: number; h1: number; h2: number }[]>;
|
||||
/** ② 부재별 fck (MPa). Absent in older files: every member uses the 공통 fck. */
|
||||
fckByMember?: FckByMember;
|
||||
/** 지반 모델 (ground_model.json)의 교각별 지반고. Absent = 불러오지 않음. Applied to a pier only by the user. */
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* 솔플레이트 규칙 (C안, 2026-11-03). Pure: the solplate every bearing of a pier gets from its rule. The rule gives the
|
||||
* 받침높이 H (solplate included), the plan size (교직 L across the 교각축 직각, 교축 W along the 교축) and the slope S (%).
|
||||
* The centre thickness T is the smallest whole mm that keeps the thin end T2 at 22 mm or more: T = ⌈22 + L·|S|/200⌉.
|
||||
* The bearing body is then h3 = H − T.
|
||||
*/
|
||||
import type { SolplatePlan, SolplateRule } from './types';
|
||||
|
||||
/** 공통 기본값 (2026-11-03): 받침높이 211 mm, 교직 1000 × 교축 1100 mm, 종단 기울기 −0.5 %. */
|
||||
export const SOLPLATE_RULE_DEFAULT: SolplateRule = { heightMm: 211, lengthMm: 1000, widthMm: 1100, slopePct: -0.5 };
|
||||
|
||||
/** Smallest allowed thickness at the thin end (mm). */
|
||||
export const SOLPLATE_MIN_T2_MM = 22;
|
||||
|
||||
/** Centre thickness T (mm, whole mm): the thin end T2 = T − L·|S|/200 is at least 22 mm. */
|
||||
export function solplateCenterT(lengthMm: number, slopePct: number): number {
|
||||
return Math.ceil(SOLPLATE_MIN_T2_MM + (lengthMm * Math.abs(slopePct)) / 200);
|
||||
}
|
||||
|
||||
/** The solplate plan of a rule: T, T1 = T + L·|S|/200, T2 = T − L·|S|/200, h3 = H − T. */
|
||||
export function solplatePlan(rule: SolplateRule): SolplatePlan {
|
||||
const T = solplateCenterT(rule.lengthMm, rule.slopePct);
|
||||
const drop = (rule.lengthMm * Math.abs(rule.slopePct)) / 200;
|
||||
return { L: rule.lengthMm, W: rule.widthMm, slopePct: rule.slopePct, T, T1: T + drop, T2: T - drop, h3: rule.heightMm - T };
|
||||
}
|
||||
|
||||
/** Input problems of a rule, in Korean; empty when the rule can build a solplate. */
|
||||
export function solplateRuleIssues(rule: SolplateRule): string[] {
|
||||
const issues: string[] = [];
|
||||
if (!(rule.lengthMm > 0) || !(rule.widthMm > 0)) issues.push('솔플레이트 교직·교축 길이는 0보다 커야 합니다.');
|
||||
if (!Number.isFinite(rule.slopePct)) issues.push('종단 기울기는 숫자여야 합니다.');
|
||||
if (!(rule.heightMm > 0)) issues.push('받침높이는 0보다 커야 합니다.');
|
||||
if (issues.length === 0) {
|
||||
const plan = solplatePlan(rule);
|
||||
if (!(rule.heightMm > plan.T)) issues.push(`받침높이는 솔플레이트 중심 두께 ${plan.T} mm보다 커야 합니다.`);
|
||||
}
|
||||
return issues;
|
||||
}
|
||||
+12
-26
@@ -1,25 +1,14 @@
|
||||
/**
|
||||
* 솔플레이트 기하 (승인 2026-10-23). Pure: the solplate of one bearing, from its block (position, rowSkew) and the
|
||||
* 공통 규칙 values. The plan is L = A + margin across the 교각축 직각 (n) and W = B + margin along the 교축 (s). The top
|
||||
* tapers across L: the thick end T1 is at the (−) side of the 교각축 직각 direction (n = −L/2), the thin end T2 at (+) (n = +L/2).
|
||||
* 솔플레이트 기하 (승인 2026-10-23, C안 2026-11-03). Pure: the solplate of one bearing, from its block (position, rowSkew)
|
||||
* and the pier's solplate plan. The plan is L across the 교각축 직각 (n) and W along the 교축 (s). The top tapers across L:
|
||||
* the thick end T1 is at the (−) side of the 교각축 직각 direction (n = −L/2), the thin end T2 at (+) (n = +L/2).
|
||||
* The underside is the bearing top (block top EL.X + h3). Lengths in m, as in the model.
|
||||
*/
|
||||
import { blockFramePoint, type BlockModel } from './geometry';
|
||||
|
||||
export interface SolplateParams {
|
||||
/** 받침 평면 A (mm, across the 교각축 직각) and B (mm, along the 교축). */
|
||||
A: number;
|
||||
B: number;
|
||||
/** 솔플 여유 (mm): L = A + margin, W = B + margin. */
|
||||
margin: number;
|
||||
/** 중심 두께 T (mm). */
|
||||
T: number;
|
||||
/** 기울기 S (%), negative as drawn: thickness changes by L·|S|/2 from the centre. */
|
||||
slopePct: number;
|
||||
}
|
||||
import type { SolplatePlan } from './types';
|
||||
|
||||
export interface SolplateGeometry {
|
||||
/** Length across the 교각축 직각 (m, = A + margin) and width along the 교축 (m, = B + margin). */
|
||||
/** Length across the 교각축 직각 (m) and width along the 교축 (m). */
|
||||
L: number;
|
||||
W: number;
|
||||
/** Thickness at the (−) side (T1) and the (+) side (T2) of the 교각축 직각 direction (m). */
|
||||
@@ -33,16 +22,13 @@ export interface SolplateGeometry {
|
||||
volume: number;
|
||||
}
|
||||
|
||||
/** Solplate of the bearing at `block`, with the bearing height h3 (mm) and the 공통 규칙 parameters. */
|
||||
export function solplateGeometry(block: BlockModel, h3: number, p: SolplateParams): SolplateGeometry {
|
||||
const L = (p.A + p.margin) / 1000; // across the 교각축 직각 (n)
|
||||
const W = (p.B + p.margin) / 1000; // along the 교축 (s)
|
||||
// Thickness change from the centre to each side: L·|S|/2, with S in % (mm).
|
||||
const drop = ((p.A + p.margin) * Math.abs(p.slopePct)) / 200 / 1000;
|
||||
const T = p.T / 1000;
|
||||
const T1 = T + drop;
|
||||
const T2 = T - drop;
|
||||
const bottom = block.padTop + h3 / 1000;
|
||||
/** Solplate of the bearing at `block`, from the pier's solplate plan (mm). */
|
||||
export function solplateGeometry(block: BlockModel, plan: SolplatePlan): SolplateGeometry {
|
||||
const L = plan.L / 1000; // across the 교각축 직각 (n)
|
||||
const W = plan.W / 1000; // along the 교축 (s)
|
||||
const T1 = plan.T1 / 1000;
|
||||
const T2 = plan.T2 / 1000;
|
||||
const bottom = block.padTop + plan.h3 / 1000;
|
||||
const thickAt = (c: number) => T1 + ((T2 - T1) * (c + L / 2)) / L;
|
||||
const corners: [number, number, number][] = [];
|
||||
for (const a of [-W / 2, W / 2]) {
|
||||
|
||||
+27
-4
@@ -48,8 +48,6 @@ export interface PierInput {
|
||||
bearings: Bearing[];
|
||||
/** Angles normalized with the imported X/Y swap. Absent in legacy saved projects = workbook frame. */
|
||||
bearingAzimuthFrame?: 'model-xy';
|
||||
/** 솔플레이트 plan per bearing (승인 2026-10-23): A, B (mm) and the slope S (%). Absent = no solplate. */
|
||||
bearingPlans?: { no: number; A: number; B: number; S: number }[];
|
||||
/** 확대기초 L (축 방향), W, 높이 h1 (mm); 비림 여유 and 비림 두께 (mm); 말뚝 외경 D and 두께 t (mm). Absent = the 1단계 constants. */
|
||||
footingLengthMm?: number;
|
||||
footingWidthMm?: number;
|
||||
@@ -60,8 +58,8 @@ export interface PierInput {
|
||||
pileThicknessMm?: number;
|
||||
/** 말뚝 종류 (규격 목록): 강관 (default) or PHC. */
|
||||
pileKind?: '강관' | 'PHC';
|
||||
/** 공통 규칙 for the solplate: centre thickness T, 솔플 여유 margin (mm), bearing height h3 (mm). */
|
||||
solplate?: { T: number; margin: number; h3: number };
|
||||
/** 솔플레이트 규칙 of this pier (C안, 2026-11-03). Absent = the default (211 · 1000 × 1100 · −0.5 %). */
|
||||
solplate?: SolplateRule;
|
||||
/** 지반·지층 (승인 2026-10-26): 위치, 지하수위 고려와 EL, 지층별 행 (위에서 아래, 행의 EL = 그 층의 하단 EL).
|
||||
* Absent = 육상, 지하수위 없음, 지층 행 없음. */
|
||||
groundPosition?: GroundPosition;
|
||||
@@ -91,6 +89,31 @@ export interface PierInput {
|
||||
strataRows?: StrataRow[];
|
||||
}
|
||||
|
||||
/** 솔플레이트 규칙 of one pier (C안, 2026-11-03): 받침높이 (solplate included), plan size (mm) and the slope S (%). */
|
||||
export interface SolplateRule {
|
||||
/** 받침높이 H (mm): from the mortar top (EL.X) to the solplate top, solplate included (H = h3 + T). */
|
||||
heightMm: number;
|
||||
/** 교직 length L (mm, across the 교각축 직각 (n)). */
|
||||
lengthMm: number;
|
||||
/** 교축 width W (mm, along the 교축 (s)). */
|
||||
widthMm: number;
|
||||
/** 기울기 S (%), negative as drawn. */
|
||||
slopePct: number;
|
||||
}
|
||||
|
||||
/** The solplate every bearing of a pier gets from its rule (mm): T is the centre thickness, h3 = H − T. */
|
||||
export interface SolplatePlan {
|
||||
L: number;
|
||||
W: number;
|
||||
slopePct: number;
|
||||
T: number;
|
||||
/** Thickness at the (−) side (T1) and the (+) side (T2) of the 교각축 직각 direction. */
|
||||
T1: number;
|
||||
T2: number;
|
||||
/** Bearing body height h3 (mm) between the mortar top and the solplate underside. */
|
||||
h3: number;
|
||||
}
|
||||
|
||||
export type GroundPosition = '육상' | '하상' | '해상';
|
||||
|
||||
export interface StrataRow {
|
||||
|
||||
+56
-309
@@ -19,13 +19,12 @@ import { shearSegments } from './core/rebar/coping';
|
||||
import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar3d';
|
||||
import { DEFAULT_MATERIAL, FCK_MEMBERS, fckChanged, fyOf, GRADE_CHOICES, IMPORTED_FCK, 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 { groundElFor, parseGroundModel, strataRowsFor, type GroundMapLayer, type GroundModelFile } from './core/ground-model';
|
||||
import { CORROSION_ALLOWANCE_MM, parseNValue, SEA_DATUM, STRATA_NAMES } from './core/ground';
|
||||
import { bearingEdgeDistances, integratedCheck, PAIR_ECCENTRICITY_LIMIT_MM, pairEccentricities } from './core/bearing-layout';
|
||||
import { bearingPlanSvg, type BearingPlanOverlay, PAIR_CHIP_LABELS } from './view/bearing-plan-svg';
|
||||
import { bearingSectionSvg, type BearingDrawingRef } from './view/bearing-section-svg';
|
||||
import { solplateDetailSvg, type SolplateDrawingRef } from './view/solplate-svg';
|
||||
import { bearingPlanSvg, PAIR_CHIP_LABELS } from './view/bearing-plan-svg';
|
||||
import { bearingSectionSvg } from './view/bearing-section-svg';
|
||||
import { solplateDetailSvg } from './view/solplate-svg';
|
||||
import { blockRebarSectionSvg } from './view/block-rebar-svg';
|
||||
import { blockRebarPlanSvg } from './view/block-rebar-plan-svg';
|
||||
import { copingEndStation, copingFrontSvg, copingSectionSvg } from './view/coping-rebar-svg';
|
||||
@@ -43,19 +42,12 @@ import { footingLegRows } from './core/rebar/footing-legs';
|
||||
import { columnElevationSvg } from './view/column-elevation-svg';
|
||||
import { type ColumnSection, columnSectionSvg } from './view/column-section-svg';
|
||||
import { groundSectionSvg } from './view/ground-svg';
|
||||
import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table';
|
||||
import { columnDiameterIssues } from './core/column-check';
|
||||
import { findPileSpec, PHC_PILES, STEEL_PILES } from './core/pile-spec';
|
||||
import { footingPlanSvg, footingSideSvg } from './view/footing-svg';
|
||||
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';
|
||||
|
||||
/** 공통 규칙 of the bearing card (2단계). Screen state for now; not yet saved to the project file. */
|
||||
let bearingRules: BearingRules = { ...BEARING_RULES_DEFAULT };
|
||||
|
||||
/** 받침 기준 for the project (Q16, 승인 2026-10-23): 'excel' by default. Drawing values per pier name. */
|
||||
let bearingBasis: BearingBasis = 'excel';
|
||||
let bearingDrawing: Record<string, DrawingBearing[]> = {};
|
||||
import { SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule';
|
||||
import type { SolplateRule } from './core/types';
|
||||
|
||||
/** 지반 모델 (ground_model.json) for the project. Its values reach a pier's 지반고 only when the user applies them. */
|
||||
let groundModel: GroundModelFile | null = null;
|
||||
@@ -288,19 +280,14 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier {
|
||||
const input = withCopingDefaults(rawInput);
|
||||
const issues: Issue[] = [];
|
||||
let model: PierModel | null = null;
|
||||
// The model uses the drawing EL.X when the project basis is 'drawing' and this pier has drawing values (Q16).
|
||||
const drawing = bearingBasis === 'drawing' ? bearingDrawing[input.name] : undefined;
|
||||
const baseInput = drawing?.length ? withDrawingBasis(input, drawing) : input;
|
||||
// Solplate plans come from the drawing rows that have A, B and S, whatever the 받침 기준 (승인 2026-10-23).
|
||||
const plans = (bearingDrawing[input.name] ?? [])
|
||||
.filter((d) => d.A !== undefined && d.B !== undefined && d.S !== undefined)
|
||||
.map((d) => ({ no: d.no, A: d.A as number, B: d.B as number, S: d.S as number }));
|
||||
const modelInput: PierInput = { ...baseInput, bearingPlans: plans, solplate: { T: bearingRules.T, margin: bearingRules.margin, h3: bearingRules.h3 } };
|
||||
if (validateInput(input, issues)) {
|
||||
// 솔플레이트 규칙 (C안, 2026-11-03): a rule that cannot make a solplate is reported, and no model is built from it.
|
||||
const solIssues = solplateRuleIssues(input.solplate ?? SOLPLATE_RULE_DEFAULT);
|
||||
for (const message of solIssues) issues.push({ code: 'S01', level: 'E', pier: input.name, message });
|
||||
if (validateInput(input, issues) && solIssues.length === 0) {
|
||||
try {
|
||||
model = buildPier(modelInput);
|
||||
model = buildPier(input);
|
||||
validatePier(input, model, issues);
|
||||
for (const message of columnDiameterIssues(modelInput.columnDiameterMm ?? 2000, model)) {
|
||||
for (const message of columnDiameterIssues(input.columnDiameterMm ?? 2000, model)) {
|
||||
issues.push({ code: 'D01', level: 'E', pier: input.name, message });
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -2108,188 +2095,15 @@ function setMaterial(key: 'fck' | GradeKey, raw: string): void {
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/** ② 교량받침 reference (1단계, 읽기 전용): the drawing's ELEVATION and 솔플레이트 tables, not used in the model (Q16). */
|
||||
/** Rebuilds every pier from its draft, so that a change of basis or drawing values reaches the model. */
|
||||
function regenerateAllPiers(): void {
|
||||
state.piers = state.piers.map((p) => regenerateEntry(p));
|
||||
setupScene();
|
||||
}
|
||||
|
||||
/** ② 받침 기준 (Q16, 승인 2026-10-23): project-wide basis, and the drawing EL.X / h1 / h2 for this pier. */
|
||||
function renderBearingBasis(wrap: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(el('p', '받침 기준 (프로젝트 전체 · 기본: 엑셀 z)', 'lbl'));
|
||||
const sel = el('select');
|
||||
sel.id = 'bearing-basis';
|
||||
for (const [value, label] of [['excel', '엑셀 z (기본)'], ['drawing', '도면 EL.X']] as const) {
|
||||
const o = el('option', label);
|
||||
o.value = value;
|
||||
sel.appendChild(o);
|
||||
}
|
||||
sel.value = bearingBasis;
|
||||
sel.addEventListener('change', () => {
|
||||
bearingBasis = sel.value as BearingBasis;
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
});
|
||||
wrap.appendChild(sel);
|
||||
|
||||
const name = pier.input.name;
|
||||
const current = new Map((bearingDrawing[name] ?? []).map((d) => [d.no, d]));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const head = el('thead');
|
||||
const hr = el('tr');
|
||||
for (const h of ['No', '열', 'EL.X (m)', 'h1 (mm)', 'h2 (mm)', '코핑 상면 차이 (mm)']) hr.appendChild(el('th', h));
|
||||
head.appendChild(hr);
|
||||
table.appendChild(head);
|
||||
const body = el('tbody');
|
||||
// The reference is the Excel-basis model: the drawing EL.X − h1 − h2 must reproduce its coping tops within 5 mm.
|
||||
const checks = bearingBasis === 'drawing' && bearingDrawing[name]?.length ? drawingCopingCheck(buildPier(pier.input), bearingDrawing[name]) : [];
|
||||
const checkOf = new Map(checks.map((c) => [c.no, c]));
|
||||
const readTable = (): DrawingBearing[] => {
|
||||
const rows: DrawingBearing[] = [];
|
||||
body.querySelectorAll<HTMLTableRowElement>('tr').forEach((tr) => {
|
||||
const inputs = tr.querySelectorAll<HTMLInputElement>('input');
|
||||
const [elx, h1, h2] = Array.from(inputs).map((i) => Number(i.value));
|
||||
const no = Number(tr.dataset.no);
|
||||
if ([elx, h1, h2].every((v) => Number.isFinite(v) && v > 0)) rows.push({ no, elx, h1, h2 });
|
||||
});
|
||||
return rows;
|
||||
};
|
||||
const commit = () => {
|
||||
bearingDrawing = { ...bearingDrawing, [name]: readTable() };
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
};
|
||||
for (const b of [...pier.input.bearings].sort((x, y) => x.no - y.no)) {
|
||||
const d = current.get(b.no);
|
||||
const tr = el('tr');
|
||||
tr.dataset.no = String(b.no);
|
||||
const cells: (string | HTMLElement)[] = [String(b.no), b.row];
|
||||
for (const [value, step] of [[d?.elx, '0.001'], [d?.h1, '1'], [d?.h2, '1']] as const) {
|
||||
const input = el('input');
|
||||
input.type = 'number';
|
||||
input.step = step;
|
||||
input.value = value === undefined ? '' : String(value);
|
||||
input.addEventListener('change', commit);
|
||||
cells.push(input);
|
||||
}
|
||||
const c = checkOf.get(b.no);
|
||||
cells.push(c === undefined ? '—' : `${c.diffMm.toFixed(1)}${c.ok ? '' : ` (허용 ${DRAWING_TOL_MM} 초과)`}`);
|
||||
tr.append(
|
||||
...cells.map((x) => {
|
||||
const td = el('td');
|
||||
if (typeof x === 'string') td.textContent = x;
|
||||
else td.appendChild(x);
|
||||
return td;
|
||||
}),
|
||||
);
|
||||
body.appendChild(tr);
|
||||
}
|
||||
table.appendChild(body);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
|
||||
// 도면 불러오기 (승인 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,.dxf';
|
||||
importInput.id = 'drawing-import';
|
||||
importInput.addEventListener('change', async () => {
|
||||
const file = importInput.files?.[0];
|
||||
if (!file) return;
|
||||
try {
|
||||
// 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 texts = readDxfTexts(await file.text());
|
||||
const r = parseBearingElevationTable(texts);
|
||||
if (r.errors.length) {
|
||||
setError(`도면 불러오기 실패: ${r.errors.slice(0, 3).join(' ')}`);
|
||||
return;
|
||||
}
|
||||
// The solplate table gives S per bearing (승인 2026-10-23); a missing S is reported, not guessed.
|
||||
const sp = parseSolplateTable(texts);
|
||||
const sByNo = new Map(sp.rows.map((x) => [x.no, x.S]));
|
||||
drawingRows = r.drawing.map((d) => ({ ...d, S: sByNo.get(d.no) }));
|
||||
if (sp.errors.length) setStatus(`솔플레이트 값 확인: ${sp.errors.slice(0, 2).join(' ')}`);
|
||||
} 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]: drawingRows };
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
setStatus(`${file.name}에서 받침 ${drawingRows.length}개의 도면 값을 불러왔습니다.`);
|
||||
} catch (err) {
|
||||
setError(`도면 파일을 읽지 못했습니다: ${(err as Error).message}`);
|
||||
} finally {
|
||||
importInput.value = '';
|
||||
}
|
||||
});
|
||||
const importLabel = el('label', '도면 파일 불러오기 (xlsx·csv)');
|
||||
importLabel.appendChild(importInput);
|
||||
wrap.appendChild(importLabel);
|
||||
const fill = el('button', '도면값 채우기 (참조 값)', 'btn small');
|
||||
fill.type = 'button';
|
||||
fill.addEventListener('click', () => {
|
||||
// The reference values include A, B (plan) and S (solplate slope), so the solplate can be drawn (승인 2026-10-23).
|
||||
bearingDrawing = { ...bearingDrawing, [name]: BEARING_ELEVATION_REF.map((r, i) => ({ no: r.no, elx: r.elx, h1: r.h1, h2: r.h2, A: r.A, B: r.B, S: SOLPLATE_REF[i].s })) };
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
});
|
||||
wrap.appendChild(fill);
|
||||
const rowsNow = bearingDrawing[name] ?? [];
|
||||
const missingPlan = pier.input.bearings.filter((b) => !rowsNow.some((d) => d.no === b.no && d.A !== undefined && d.B !== undefined && d.S !== undefined)).length;
|
||||
if (missingPlan > 0) {
|
||||
wrap.appendChild(el('p', `솔플레이트 값(A, B, S)이 없는 받침 ${missingPlan}개는 솔플레이트를 그리지 않습니다. 도면을 불러오거나 "도면값 채우기"를 누르세요.`, 'warn-text'));
|
||||
}
|
||||
if (bearingBasis === 'drawing' && !bearingDrawing[name]?.length) {
|
||||
wrap.appendChild(el('p', '도면 값이 없어서 엑셀 z로 계산 중입니다.', 'warn-text'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 받침 배치 (승인 2026-10-26, read-only): C4·C5 from the model (coping edge ↔ bearing centre, 교각축 방향) and the
|
||||
* eccentricity of each front–back pair (일체 allowed up to 100 mm, otherwise 분리 first).
|
||||
*/
|
||||
/** 상세 보기 pair shown in the plan, and whether the drawing values are overlaid (screen state only, 승인 2026-10-26). */
|
||||
/** 상세 보기 pair shown in the plan (screen state only, 승인 2026-10-26). */
|
||||
let bearingPairIndex = 0;
|
||||
let bearingOverlayOn = true;
|
||||
/** ③ 받침블럭 view: the C–C section or the plan (승인 2026-10-26). */
|
||||
let bearingRebarView: 'section' | 'plan' = 'section';
|
||||
|
||||
/**
|
||||
* Drawing values of one bearing for the overlays (승인 2026-10-26): the imported drawing row where it has the value
|
||||
* (elx, h1, h2, A, B), otherwise the reference table (h3, h4, E).
|
||||
*/
|
||||
function drawingRefFor(pier: Pier, no: number): BearingDrawingRef | undefined {
|
||||
const r = BEARING_ELEVATION_REF.find((x) => x.no === no);
|
||||
if (!r) return undefined;
|
||||
const d = (bearingDrawing[pier.input.name] ?? []).find((x) => x.no === no);
|
||||
const A = d?.A ?? r.A;
|
||||
const B = d?.B ?? r.B;
|
||||
return { elx: d?.elx ?? r.elx, h1: d?.h1 ?? r.h1, h2: d?.h2 ?? r.h2, h3: r.h3, h4: r.h4, E: r.E, A, B };
|
||||
}
|
||||
|
||||
/** Drawing A, B of the front and back bearings of a pair, for the plan overlay. */
|
||||
function overlayFor(pier: Pier, frontNo: number, backNo: number): BearingPlanOverlay | undefined {
|
||||
const f = drawingRefFor(pier, frontNo);
|
||||
const b = drawingRefFor(pier, backNo);
|
||||
return f && b ? { front: { A: f.A, B: f.B }, back: { A: b.A, B: b.B } } : undefined;
|
||||
}
|
||||
|
||||
/** Drawing solplate values of one bearing (reference table), for the solplate detail overlay. */
|
||||
function solplateRefFor(no: number): SolplateDrawingRef | undefined {
|
||||
const r = SOLPLATE_REF.find((x) => x.no === no);
|
||||
return r ? { L: r.L, W: r.W, T1: r.T1, T2: r.T2, S: r.s } : undefined;
|
||||
}
|
||||
|
||||
function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
|
||||
if (!pier.model) return;
|
||||
// 상세 보기 (승인 2026-10-26): pair chips (①·⑥ …) and the plan with the drawing values overlaid as dashed outlines.
|
||||
@@ -2308,39 +2122,26 @@ function renderBearingLayout(wrap: HTMLDivElement, pier: Pier): void {
|
||||
chips.appendChild(chip);
|
||||
});
|
||||
wrap.appendChild(chips);
|
||||
const overlayCheck = el('input');
|
||||
overlayCheck.type = 'checkbox';
|
||||
overlayCheck.checked = bearingOverlayOn;
|
||||
overlayCheck.id = 'bearing-overlay';
|
||||
overlayCheck.addEventListener('change', () => {
|
||||
bearingOverlayOn = overlayCheck.checked;
|
||||
renderAll();
|
||||
});
|
||||
const overlayLabel = el('label', ' 도면값 겹쳐 보기 (점선)');
|
||||
overlayLabel.prepend(overlayCheck);
|
||||
wrap.appendChild(overlayLabel);
|
||||
const current = pairList[bearingPairIndex];
|
||||
if (current) {
|
||||
const fig = el('figure', undefined, 'ground-fig');
|
||||
const overlay = bearingOverlayOn ? overlayFor(pier, current.frontNo, current.backNo) : undefined;
|
||||
fig.innerHTML = bearingPlanSvg(pier.model, bearingPairIndex, overlay);
|
||||
fig.appendChild(el('figcaption', '평면 · 실선 = 모델(솔플레이트, 블록 윤곽) · 점선 = 도면값 · C4·C5는 교축 방향, 받침 중심에서 블록 바깥 연단까지', 'muted small'));
|
||||
fig.innerHTML = bearingPlanSvg(pier.model, bearingPairIndex);
|
||||
fig.appendChild(el('figcaption', '평면 · 실선 = 모델(솔플레이트, 블록 윤곽) · C4·C5는 교축 방향, 받침 중심에서 블록 바깥 연단까지', 'muted small'));
|
||||
wrap.appendChild(fig);
|
||||
|
||||
// 단면 (elevation): the blocks, mortar, bearing body and solplates; the drawing heights dashed.
|
||||
// 단면 (elevation): the blocks, mortar, bearing body and solplates.
|
||||
const section = el('figure', undefined, 'ground-fig');
|
||||
const refs = bearingOverlayOn ? { front: drawingRefFor(pier, current.frontNo), back: drawingRefFor(pier, current.backNo) } : undefined;
|
||||
section.innerHTML = bearingSectionSvg(pier.model, bearingPairIndex, refs);
|
||||
section.appendChild(el('figcaption', '단면 · EL.X는 모르타르 상단, E = h1 + h2 + h3 + h4 · 점선 = 도면 높이', 'muted small'));
|
||||
section.innerHTML = bearingSectionSvg(pier.model, bearingPairIndex);
|
||||
section.appendChild(el('figcaption', '단면 · EL.X는 모르타르 상단 · h3 = 받침 본체 · 솔플레이트 T1(−측)·T2(+측)', 'muted small'));
|
||||
wrap.appendChild(section);
|
||||
|
||||
// 솔플레이트 상세 for the two bearings of the pair (plan and taper, drawing dashed).
|
||||
// 솔플레이트 상세 for the two bearings of the pair (plan and taper).
|
||||
const solGrid = el('div', undefined, 'footing-grid');
|
||||
for (const no of [current.frontNo, current.backNo]) {
|
||||
const block = pier.model.blocks.find((b) => b.no === no)!;
|
||||
const fig2 = el('figure', undefined, 'footing-fig');
|
||||
fig2.innerHTML = solplateDetailSvg(block, bearingOverlayOn ? solplateRefFor(no) : undefined);
|
||||
fig2.appendChild(el('figcaption', `솔플레이트 상세 · ${no}번 · 두께 과장 · 점선 = 도면값`, 'muted small'));
|
||||
fig2.innerHTML = solplateDetailSvg(block);
|
||||
fig2.appendChild(el('figcaption', `솔플레이트 상세 · ${no}번 · 두께 과장`, 'muted small'));
|
||||
solGrid.appendChild(fig2);
|
||||
}
|
||||
wrap.appendChild(solGrid);
|
||||
@@ -2618,9 +2419,7 @@ function renderGroundModel(box: HTMLDivElement, pier: Pier): void {
|
||||
}
|
||||
|
||||
function renderBearingRef(box: HTMLDivElement, pier: Pier): void {
|
||||
const m = pier.model;
|
||||
const wrap = el('div', undefined, 'bearing-ref');
|
||||
renderBearingBasis(wrap, pier);
|
||||
// 받침 종류·용량 (승인 2026-10-26: 글자 입력, 기본 탄성받침). Saved in the project; no rule yet, so it does not change the model.
|
||||
wrap.appendChild(el('p', '받침 종류·용량 (글자 입력 · 기본 탄성받침)', 'lbl'));
|
||||
const typeInput = el('input');
|
||||
@@ -2636,98 +2435,52 @@ function renderBearingRef(box: HTMLDivElement, pier: Pier): void {
|
||||
});
|
||||
wrap.appendChild(typeInput);
|
||||
renderBearingLayout(wrap, pier);
|
||||
wrap.appendChild(el('p', '공통 규칙 (입력 · 기본값 = 도면값)', 'lbl'));
|
||||
const rules: [keyof BearingRules, string][] = [['h2', '모르타르 h2 (mm)'], ['h3', '받침 높이 h3 (mm)'], ['T', '솔플레이트 중심 T (mm)'], ['margin', '솔플 여유 (mm)']];
|
||||
renderSolplateRule(wrap, pier);
|
||||
box.appendChild(wrap);
|
||||
}
|
||||
|
||||
/** Sets one pier's solplate rule (C안, 2026-11-03) and rebuilds that pier. */
|
||||
function setSolplateRule(name: string, rule: SolplateRule): void {
|
||||
const p = state.piers.find((q) => q.input.name === name);
|
||||
if (!p) return;
|
||||
p.draft = { ...p.draft, solplate: rule };
|
||||
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
|
||||
setupScene();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/** 솔플레이트 규칙 (C안, 2026-11-03): per pier. 받침높이 H (solplate included), the plan L × W and the slope S; T and h3 follow. */
|
||||
function renderSolplateRule(wrap: HTMLDivElement, pier: Pier): void {
|
||||
const rule = pier.draft.solplate ?? SOLPLATE_RULE_DEFAULT;
|
||||
wrap.appendChild(el('p', '솔플레이트 규칙 (이 교각 · 기본 211 · 1000 × 1100 · −0.5 %)', 'lbl'));
|
||||
const fields: [keyof SolplateRule, string][] = [
|
||||
['heightMm', '받침높이 H (솔플레이트 포함, mm)'],
|
||||
['lengthMm', '솔플레이트 교직 L (mm)'],
|
||||
['widthMm', '솔플레이트 교축 W (mm)'],
|
||||
['slopePct', '종단 기울기 S (%)'],
|
||||
];
|
||||
const grid = el('div', undefined, 'edit-row');
|
||||
for (const [key, label] of rules) {
|
||||
const id = `bear-${key}`;
|
||||
for (const [key, label] of fields) {
|
||||
const id = `sol-${key}`;
|
||||
const lab = el('label', label);
|
||||
lab.htmlFor = id;
|
||||
const input = el('input');
|
||||
input.id = id;
|
||||
input.type = 'number';
|
||||
input.step = 'any';
|
||||
input.value = String(bearingRules[key]);
|
||||
const changed = bearingRules[key] !== BEARING_RULES_DEFAULT[key];
|
||||
if (changed) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => {
|
||||
const v = Number(input.value);
|
||||
const min = key === 'margin' ? 0 : 0.0001;
|
||||
if (!Number.isFinite(v) || v < min) {
|
||||
setError(`${label}은(는) ${key === 'margin' ? '0 이상' : '0보다 큰'} 숫자여야 합니다.`);
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
bearingRules = { ...bearingRules, [key]: v };
|
||||
regenerateAllPiers();
|
||||
renderAll();
|
||||
});
|
||||
grid.append(lab, input, el('span', `기본 ${BEARING_RULES_DEFAULT[key]}`, 'muted small'));
|
||||
input.value = String(rule[key]);
|
||||
if (rule[key] !== SOLPLATE_RULE_DEFAULT[key]) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => setSolplateRule(pier.input.name, { ...rule, [key]: Number(input.value) }));
|
||||
grid.append(lab, input, el('span', `기본 ${SOLPLATE_RULE_DEFAULT[key]}`, 'muted small'));
|
||||
}
|
||||
wrap.appendChild(grid);
|
||||
wrap.appendChild(el('p', '규칙 적용 값 (E = h1 + h2 + h3 + h4, L = A + 여유, W = B + 여유, T1·T2 = T ± L·|S|/2)', 'lbl'));
|
||||
const ct = el('table', undefined, 'grid-table rebar-table');
|
||||
const ch = el('thead');
|
||||
const chr = el('tr');
|
||||
for (const h of ['No', '열', 'E', 'L', 'W', 'T1 (−측)', 'T2 (+측)', '도면 대비']) chr.appendChild(el('th', h));
|
||||
ch.appendChild(chr);
|
||||
ct.appendChild(ch);
|
||||
const cb = el('tbody');
|
||||
for (const r of BEARING_ELEVATION_REF) {
|
||||
const v = applyBearingRules(r, bearingRules);
|
||||
const d = applyBearingRules(r, BEARING_RULES_DEFAULT);
|
||||
const same = v.E === d.E && v.L === d.L && v.W === d.W && v.T1 === d.T1 && v.T2 === d.T2;
|
||||
cb.appendChild(el('tr')).append(
|
||||
el('td', String(r.no)), el('td', r.row), el('td', String(v.E)), el('td', String(v.L)), el('td', String(v.W)),
|
||||
el('td', String(v.T1)), el('td', String(v.T2)), el('td', same ? '같음' : '규칙 변경'),
|
||||
);
|
||||
const issues = solplateRuleIssues(rule);
|
||||
if (issues.length > 0) {
|
||||
wrap.appendChild(el('p', issues.join(' '), 'warn-text'));
|
||||
return;
|
||||
}
|
||||
ct.appendChild(cb);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(ct);
|
||||
wrap.appendChild(el('p', '교량받침 ELEVATION TABLE (도면값 · 읽기 전용, mm · EL m)', 'lbl'));
|
||||
wrap.appendChild(el('p', '엑셀 z를 EL.X로 씁니다(몰탈 50 mm 포함 받침블럭 상면, Q16 결정 2026-10-23). 도면 EL.X와의 차이는 "도면 ≠ 엑셀"로만 표시합니다.', 'muted small'));
|
||||
const front = m?.blocks.filter((b) => b.row === '전열') ?? [];
|
||||
const backBlocks = m?.blocks.filter((b) => b.row === '후열') ?? [];
|
||||
const et = el('table', undefined, 'grid-table rebar-table');
|
||||
const eh = el('thead');
|
||||
const ehr = el('tr');
|
||||
for (const h of ['No', '열', '엑셀 z (= EL.X, 모델)', '도면 EL.X', '도면 − 엑셀 (mm)', 'E', 'A', 'B', 'C1', 'S1']) ehr.appendChild(el('th', h));
|
||||
eh.appendChild(ehr);
|
||||
et.appendChild(eh);
|
||||
const eb = el('tbody');
|
||||
for (const r of BEARING_ELEVATION_REF) {
|
||||
// Excel z is EL.X (Q16 decision 2026-10-23, option 1); the drawing's EL.X is shown for comparison only.
|
||||
const block = r.row === '전열' ? front[r.no - 1] : backBlocks[r.no - 6];
|
||||
const excelZ = block?.padTop;
|
||||
const diffMm = excelZ === undefined ? NaN : Math.round((r.elx - excelZ) * 1000);
|
||||
eb.appendChild(el('tr')).append(
|
||||
el('td', String(r.no)), el('td', r.row), el('td', excelZ === undefined ? '—' : excelZ.toFixed(3)), el('td', r.elx.toFixed(3)),
|
||||
el('td', Number.isNaN(diffMm) ? '—' : String(diffMm)), el('td', String(r.E)),
|
||||
el('td', String(r.A)), el('td', String(r.B)), el('td', String(r.C1)), el('td', String(r.S1)),
|
||||
);
|
||||
}
|
||||
et.appendChild(eb);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(et);
|
||||
|
||||
wrap.appendChild(el('p', '솔플레이트 TABLE (도면값, mm)', 'lbl'));
|
||||
const st = el('table', undefined, 'grid-table rebar-table');
|
||||
const sh = el('thead');
|
||||
const shr = el('tr');
|
||||
for (const h of ['No', 'S (%)', 'L', 'W', 'T (중심)', 'T1 (−측)', 'T2 (+측)', '식 검산']) shr.appendChild(el('th', h));
|
||||
sh.appendChild(shr);
|
||||
st.appendChild(sh);
|
||||
const sb = el('tbody');
|
||||
for (const r of SOLPLATE_REF) {
|
||||
const calc = solplateTs(SOLPLATE_T_RULE, r.L, r.s);
|
||||
const ok = calc.T1 === r.T1 && calc.T2 === r.T2;
|
||||
sb.appendChild(el('tr')).append(
|
||||
el('td', String(r.no)), el('td', r.s.toFixed(3)), el('td', String(r.L)), el('td', String(r.W)),
|
||||
el('td', String(r.T)), el('td', String(r.T1)), el('td', String(r.T2)), el('td', ok ? '일치' : `식 ${calc.T1} / ${calc.T2}`),
|
||||
);
|
||||
}
|
||||
st.appendChild(sb);
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(st);
|
||||
box.appendChild(wrap);
|
||||
const plan = solplatePlan(rule);
|
||||
wrap.appendChild(el('p', `중심 두께 T = ${plan.T} mm (T2 ≥ 22 mm) · 받침 본체 h3 = H − T = ${plan.h3} mm · T1 = ${plan.T1.toFixed(1)} mm · T2 = ${plan.T2.toFixed(1)} mm`, 'muted small'));
|
||||
}
|
||||
|
||||
/** ② 코핑 상세 (2단계, 읽기 전용): d_IP2 and the 계단 구간 표 from the model (승인 2026-10-23). */
|
||||
@@ -2850,8 +2603,6 @@ function snapshot(): string {
|
||||
piers: state.piers.map((p) => ({ input: p.input, defaults: p.defaults, draft: p.draft })),
|
||||
material: state.material,
|
||||
fckByMember: state.fckByMember,
|
||||
bearingBasis,
|
||||
bearingDrawing,
|
||||
groundModel,
|
||||
});
|
||||
}
|
||||
@@ -2869,8 +2620,6 @@ function currentProjectText(): string {
|
||||
history: state.history,
|
||||
common: state.material,
|
||||
fckByMember: state.fckByMember,
|
||||
bearingBasis,
|
||||
bearingDrawing,
|
||||
groundModel: groundModel ?? undefined,
|
||||
});
|
||||
}
|
||||
@@ -3002,8 +2751,6 @@ function openProjectText(text: string, saved: boolean): void {
|
||||
state.history = parsed.history;
|
||||
state.material = materialFrom(parsed.common);
|
||||
state.fckByMember = { ...parsed.fckByMember };
|
||||
bearingBasis = parsed.bearingBasis ?? 'excel';
|
||||
bearingDrawing = Object.fromEntries(Object.entries(parsed.bearingDrawing ?? {}).map(([k, v]) => [k, [...v]]));
|
||||
groundModel = parsed.groundModel ?? null;
|
||||
groundMapMode = groundModel?.mapLayers?.length ? 'satellite' : 'none';
|
||||
setGroundMapMode(groundMapMode);
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import type { PierModel } from '../core/geometry';
|
||||
import { bearingEdgeDistances, pairEccentricities } from '../core/bearing-layout';
|
||||
|
||||
/** Drawing values (A, B of the solplate, mm) of the front and back bearings, to overlay on the model (승인 2026-10-26). */
|
||||
export interface BearingPlanOverlay {
|
||||
front: { A: number; B: number };
|
||||
back: { A: number; B: number };
|
||||
}
|
||||
|
||||
/** Number of pairs the 상세 보기 chips can show (①–⑩ in the mockup). */
|
||||
export const PAIR_CHIP_LABELS = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
|
||||
|
||||
/**
|
||||
* 교량받침 상세 · 평면 for one pair (승인 2026-10-26): the bearings keep their actual 교각축 positions (the offset
|
||||
* between the two bearings is shown as it is), and the blocks and solplates follow them. The model's solplates are solid, the drawing values
|
||||
* dashed. 일체 draws one 2500 × 1200 outline; 분리 draws two 1200 × 1200 outlines. Dimensions: C1 (bearing to block edge),
|
||||
* between the two bearings is shown as it is), and the blocks and solplates follow them. 일체 draws one 2500 × 1200 outline; 분리 draws two 1200 × 1200 outlines. Dimensions: C1 (bearing to block edge),
|
||||
* C2 (between the bearings), C3 (block span). C4 and C5 are the outer margins along the 교축.
|
||||
*/
|
||||
/**
|
||||
@@ -24,7 +17,7 @@ function rect(cx: number, cy: number, w: number, h: number, skewDeg: number, att
|
||||
return `<rect x="${cx - w / 2}" y="${cy - h / 2}" width="${w}" height="${h}" transform="rotate(${-skewDeg} ${cx} ${cy})" ${attrs}></rect>`;
|
||||
}
|
||||
|
||||
export function bearingPlanSvg(m: PierModel, pairIndex: number, overlay?: BearingPlanOverlay): string {
|
||||
export function bearingPlanSvg(m: PierModel, pairIndex: number): string {
|
||||
const pair = pairEccentricities(m.blocks)[pairIndex];
|
||||
if (!pair) return '';
|
||||
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
|
||||
@@ -55,18 +48,9 @@ export function bearingPlanSvg(m: PierModel, pairIndex: number, overlay?: Bearin
|
||||
}
|
||||
}
|
||||
|
||||
// Solplates: the model's plan (A across the 교축, B along the 교각축), centred on each bearing.
|
||||
// Solplates: the pier's plan (L across the 교축, W along the 교각축), centred on each bearing.
|
||||
for (const b of [f, k]) {
|
||||
if (b.plan) {
|
||||
const { A, B } = b.plan;
|
||||
parts.push(rect(X(b.n), Y(b.s), A, B, b.rowSkewDeg, `fill="#EEF3F7" stroke="#2F6DB5" stroke-width="10"`));
|
||||
}
|
||||
}
|
||||
// Drawing values overlaid (dashed) on the same bearings (#1 겹쳐 보이게).
|
||||
if (overlay) {
|
||||
for (const [b, v] of [[f, overlay.front], [k, overlay.back]] as const) {
|
||||
parts.push(rect(X(b.n), Y(b.s), v.A, v.B, b.rowSkewDeg, `fill="none" stroke="#B4531F" stroke-width="10" stroke-dasharray="40 25"`));
|
||||
}
|
||||
parts.push(rect(X(b.n), Y(b.s), b.plan.L, b.plan.W, b.rowSkewDeg, `fill="#EEF3F7" stroke="#2F6DB5" stroke-width="10"`));
|
||||
}
|
||||
|
||||
// Bearing centres and numbers.
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
import type { PierModel } from '../core/geometry';
|
||||
import { pairEccentricities } from '../core/bearing-layout';
|
||||
|
||||
/**
|
||||
* Drawing values of one bearing (도면값), overlaid as dashed outlines (승인 2026-10-26).
|
||||
* All lengths in mm; elx in m.
|
||||
*/
|
||||
export interface BearingDrawingRef {
|
||||
elx?: number;
|
||||
h1: number;
|
||||
h2: number;
|
||||
h3: number;
|
||||
h4: number;
|
||||
E: number;
|
||||
A: number;
|
||||
B: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 교량받침 상세 · 단면 (elevation) for one pair (승인 2026-10-26): the model's blocks, mortar, bearing body,
|
||||
* and solplate per bearing, with the drawing heights overlaid as dashed outlines. x is the 교축 (n), y the EL (up).
|
||||
* Labels: EL.X (mortar top), h1 (block), h2 (mortar), h3 (bearing), h4 (solplate), E = h1 + h2 + h3 + h4.
|
||||
* and solplate per bearing. x is the 교축 (n), y the EL (up). Labels: EL.X (mortar top), h1 (block), h3 (bearing body),
|
||||
* T (solplate, taper T1 at the (−) side, T2 at the (+) side).
|
||||
*/
|
||||
export function bearingSectionSvg(m: PierModel, pairIndex: number, refs?: { front?: BearingDrawingRef; back?: BearingDrawingRef }): string {
|
||||
export function bearingSectionSvg(m: PierModel, pairIndex: number): string {
|
||||
const pair = pairEccentricities(m.blocks)[pairIndex];
|
||||
if (!pair) return '';
|
||||
const f = m.blocks.find((b) => b.no === pair.frontNo && b.row === '전열')!;
|
||||
@@ -46,37 +31,19 @@ export function bearingSectionSvg(m: PierModel, pairIndex: number, refs?: { fron
|
||||
}
|
||||
|
||||
// Mortar, bearing body and solplate per bearing, from the model.
|
||||
for (const [b, ref] of [[f, refs?.front], [k, refs?.back]] as const) {
|
||||
for (const b of [f, k]) {
|
||||
const pad = 1000;
|
||||
parts.push(`<rect x="${X(b.n) - pad / 2}" y="${Y(b.padTop)}" width="${pad}" height="${50}" fill="#B7C7D3" stroke="#6E8496" stroke-width="6"></rect>`);
|
||||
if (b.plan) {
|
||||
const { A, margin, h3, T } = b.plan;
|
||||
const L = A + margin;
|
||||
const bodyTop = b.padTop + h3 / 1000;
|
||||
parts.push(`<rect x="${X(b.n) - (L - 200) / 2}" y="${Y(bodyTop)}" width="${L - 200}" height="${h3}" fill="#8898AA" stroke="#5B6676" stroke-width="6"></rect>`);
|
||||
const drop = (L * Math.abs(b.plan.S)) / 200;
|
||||
const t1 = T + drop;
|
||||
const t2 = T - drop;
|
||||
const x0 = X(b.n) - L / 2;
|
||||
const x1 = X(b.n) + L / 2;
|
||||
parts.push(`<polygon points="${x0},${Y(bodyTop)} ${x1},${Y(bodyTop)} ${x1},${Y(bodyTop + t2 / 1000)} ${x0},${Y(bodyTop + t1 / 1000)}" fill="#F3D9B8" stroke="#B4531F" stroke-width="6"></polygon>`);
|
||||
}
|
||||
if (ref) {
|
||||
// Drawing heights from the block bottom, dashed (overlay).
|
||||
const base = b.bottom;
|
||||
const dBlockTop = base + (ref.h1 ?? 0) / 1000;
|
||||
const dPad = dBlockTop + ref.h2 / 1000;
|
||||
const dBody = dPad + ref.h3 / 1000;
|
||||
const dSol = dBody + ref.h4 / 1000;
|
||||
parts.push(`<rect x="${X(b.n) - 600}" y="${Y(dBlockTop)}" width="1200" height="${(dBlockTop - base) * 1000}" fill="none" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></rect>`);
|
||||
parts.push(`<line x1="${X(b.n) - 700}" y1="${Y(dSol)}" x2="${X(b.n) + 700}" y2="${Y(dSol)}" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></line>`);
|
||||
parts.push(`<line x1="${X(b.n) - 700}" y1="${Y(dBody)}" x2="${X(b.n) + 700}" y2="${Y(dBody)}" stroke="#B4531F" stroke-width="8" stroke-dasharray="40 25"></line>`);
|
||||
}
|
||||
const { L, h3, T1, T2 } = b.plan;
|
||||
const bodyTop = b.padTop + h3 / 1000;
|
||||
parts.push(`<rect x="${X(b.n) - (L - 200) / 2}" y="${Y(bodyTop)}" width="${L - 200}" height="${h3}" fill="#8898AA" stroke="#5B6676" stroke-width="6"></rect>`);
|
||||
const x0 = X(b.n) - L / 2;
|
||||
const x1 = X(b.n) + L / 2;
|
||||
parts.push(`<polygon points="${x0},${Y(bodyTop)} ${x1},${Y(bodyTop)} ${x1},${Y(bodyTop + T2 / 1000)} ${x0},${Y(bodyTop + T1 / 1000)}" fill="#F3D9B8" stroke="#B4531F" stroke-width="6"></polygon>`);
|
||||
// EL.X at the mortar top, bearing number.
|
||||
parts.push(`<circle cx="${X(b.n)}" cy="${Y(b.padTop)}" r="30" fill="#18212B"></circle>`);
|
||||
parts.push(`<text x="${X(b.n) + 60}" y="${Y(b.padTop) - 60}" font-size="90" fill="#18212B">${b.no} · EL.X ${b.padTop.toFixed(3)}</text>`);
|
||||
parts.push(`<text x="${X(b.n)}" y="${Y(b.bottom) + 160}" text-anchor="middle" font-size="90" fill="#55606E">h1 ${Math.round((b.top - b.bottom) * 1000)}</text>`);
|
||||
}
|
||||
return `<svg viewBox="-2000 -1200 4000 2000" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="교량받침 상세 단면: 블록, 모르타르, 받침, 솔플레이트, EL.X, 도면 높이 점선">${parts.join('')}</svg>`;
|
||||
return `<svg viewBox="-2000 -1200 4000 2000" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="교량받침 상세 단면: 블록, 모르타르, 받침, 솔플레이트, EL.X">${parts.join('')}</svg>`;
|
||||
}
|
||||
|
||||
+5
-5
@@ -152,14 +152,14 @@ export function buildPierScene(m: PierModel, terrain?: TerrainGrid, groundEl: nu
|
||||
pad.rotateY(turn);
|
||||
pad.translate(b.s, b.padTop - 0.025, b.n);
|
||||
add('pads', pad, 1, b.k);
|
||||
if (b.plan) {
|
||||
// 교량받침 본체 (승인 2026-10-23): the solplate plan less 100 mm on each side, from the mortar top up to the solplate.
|
||||
const body = new THREE.BoxGeometry((b.plan.B + b.plan.margin) / 1000 - 0.2, b.plan.h3 / 1000, (b.plan.A + b.plan.margin) / 1000 - 0.2);
|
||||
{
|
||||
// 교량받침 본체: the solplate plan less 100 mm on each side, from the mortar top up to the solplate.
|
||||
const body = new THREE.BoxGeometry(b.plan.W / 1000 - 0.2, b.plan.h3 / 1000, b.plan.L / 1000 - 0.2);
|
||||
body.rotateY(turn);
|
||||
body.translate(b.s, b.padTop + b.plan.h3 / 2000, b.n);
|
||||
add('bearings', body, 1, b.k);
|
||||
// Solplate: a wedge that tapers across the 교각축 직각 (T1 at the (−) side), on the bearing (승인 2026-10-23).
|
||||
const sp = solplateGeometry(b, b.plan.h3, { A: b.plan.A, B: b.plan.B, margin: b.plan.margin, T: b.plan.T, slopePct: b.plan.S });
|
||||
// Solplate: a wedge that tapers across the 교각축 직각 (T1 at the (−) side), on the bearing.
|
||||
const sp = solplateGeometry(b, b.plan);
|
||||
add('solplates', wedgeGeometry(sp.corners), 1, b.k);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,46 +1,24 @@
|
||||
import type { BlockModel } from '../core/geometry';
|
||||
|
||||
/** Drawing values of one solplate (도면값), overlaid as dashed outlines. mm. */
|
||||
export interface SolplateDrawingRef {
|
||||
L: number;
|
||||
W: number;
|
||||
T1: number;
|
||||
T2: number;
|
||||
S: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 솔플레이트 상세 (승인 2026-10-26) for one bearing: the plan (L across the 교축, W along the 교각축) and the
|
||||
* taper in side view (T1 at the (−) side, T2 at the (+) side, S % · T). Model solid, drawing dashed.
|
||||
* 솔플레이트 상세 (승인 2026-10-26, C안 2026-11-03) for one bearing: the plan (L across, W along the 교각축) and the
|
||||
* taper in side view (T1 at the (−) side, T2 at the (+) side, S % · T). Thickness is exaggerated for legibility.
|
||||
*/
|
||||
export function solplateDetailSvg(block: BlockModel, ref?: SolplateDrawingRef): string {
|
||||
export function solplateDetailSvg(block: BlockModel): string {
|
||||
const plan = block.plan;
|
||||
if (!plan) return '';
|
||||
const L = plan.A + plan.margin;
|
||||
const W = plan.B + plan.margin;
|
||||
const drop = (L * Math.abs(plan.S)) / 200;
|
||||
const t1 = plan.T + drop;
|
||||
const t2 = plan.T - drop;
|
||||
const { L, W, T1: t1, T2: t2 } = plan;
|
||||
const parts: string[] = [];
|
||||
// Plan (left): L across, W along.
|
||||
parts.push(`<rect x="${-L / 2}" y="${-W / 2}" width="${L}" height="${W}" fill="#F3D9B8" stroke="#B4531F" stroke-width="8"></rect>`);
|
||||
parts.push(`<text x="0" y="${-W / 2 - 40}" text-anchor="middle" font-size="60" fill="#18212B">L ${L.toFixed(0)}</text>`);
|
||||
parts.push(`<text x="${L / 2 + 40}" y="0" font-size="60" fill="#18212B">W ${W.toFixed(0)}</text>`);
|
||||
if (ref) {
|
||||
parts.push(`<rect x="${-ref.L / 2}" y="${-ref.W / 2}" width="${ref.L}" height="${ref.W}" fill="none" stroke="#B4531F" stroke-width="6" stroke-dasharray="40 25"></rect>`);
|
||||
}
|
||||
// Side view (right): thickness T1 at x = −L/2 and T2 at x = +L/2, shown with the taper.
|
||||
const ox = L / 2 + 500;
|
||||
const sc = 4; // exaggerated thickness for legibility, labelled
|
||||
parts.push(`<polygon points="${ox - L / 2},0 ${ox + L / 2},0 ${ox + L / 2},${-t2 * sc} ${ox - L / 2},${-t1 * sc}" fill="#F3D9B8" stroke="#B4531F" stroke-width="8"></polygon>`);
|
||||
parts.push(`<text x="${ox - L / 2}" y="${-t1 * sc - 40}" font-size="60" fill="#18212B">T1 ${t1.toFixed(0)}</text>`);
|
||||
parts.push(`<text x="${ox + L / 2}" y="${-t2 * sc - 40}" text-anchor="end" font-size="60" fill="#18212B">T2 ${t2.toFixed(0)}</text>`);
|
||||
parts.push(`<text x="${ox}" y="120" text-anchor="middle" font-size="60" fill="#55606E">S ${plan.S} % · T ${plan.T} (두께 과장 ×${sc})</text>`);
|
||||
if (ref) {
|
||||
const r1 = ref.T1;
|
||||
const r2 = ref.T2;
|
||||
parts.push(`<polygon points="${ox - ref.L / 2},0 ${ox + ref.L / 2},0 ${ox + ref.L / 2},${-r2 * sc} ${ox - ref.L / 2},${-r1 * sc}" fill="none" stroke="#B4531F" stroke-width="6" stroke-dasharray="40 25"></polygon>`);
|
||||
}
|
||||
parts.push(`<text x="${ox - L / 2}" y="${-t1 * sc - 40}" font-size="60" fill="#18212B">T1 ${t1.toFixed(1)}</text>`);
|
||||
parts.push(`<text x="${ox + L / 2}" y="${-t2 * sc - 40}" text-anchor="end" font-size="60" fill="#18212B">T2 ${t2.toFixed(1)}</text>`);
|
||||
parts.push(`<text x="${ox}" y="120" text-anchor="middle" font-size="60" fill="#55606E">S ${plan.slopePct} % · T ${plan.T} (두께 과장 ×${sc})</text>`);
|
||||
const minX = -L / 2 - 200;
|
||||
const maxX = ox + L / 2 + 200;
|
||||
return `<svg viewBox="${minX} -900 ${maxX - minX} 1300" width="100%" style="max-width: 560px; height: auto" role="img" aria-label="솔플레이트 상세: 평면 L × W, 두께 T1 · T2 (두께 과장)">${parts.join('')}</svg>`;
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { BEARING_ELEVATION_REF, DRAWING_TOL_MM, drawingCopingCheck, withDrawingBasis, type DrawingBearing } from '../src/core/bearing-ref';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
const drawing: DrawingBearing[] = BEARING_ELEVATION_REF.map((r) => ({ no: r.no, elx: r.elx, h1: r.h1, h2: r.h2 }));
|
||||
|
||||
describe('받침 기준: 도면 EL.X (Q16 option 2, 승인 2026-10-23)', () => {
|
||||
it('the default basis (엑셀 z) keeps the model as before: the drawing check still reports the EL.X gap', () => {
|
||||
const excel = buildPier(loadP1());
|
||||
expect(excel.blocks.filter((b) => b.row === '전열')[0].padTop).toBeCloseTo(43.561, 3);
|
||||
});
|
||||
|
||||
it('with the drawing basis the pads sit at the drawing EL.X and the coping tops stay within the 5 mm tolerance', () => {
|
||||
const m = buildPier(withDrawingBasis(loadP1(), drawing));
|
||||
expect(m.blocks.filter((b) => b.row === '전열')[0].padTop).toBeCloseTo(43.625, 3);
|
||||
for (const c of drawingCopingCheck(m, drawing)) {
|
||||
expect(Math.abs(c.diffMm), `No ${c.no}`).toBeLessThanOrEqual(DRAWING_TOL_MM);
|
||||
}
|
||||
});
|
||||
|
||||
it('with the drawing basis the front block (h1) is 164 mm and the back block 139 mm, excluding the 50 mm pad', () => {
|
||||
const m = buildPier(withDrawingBasis(loadP1(), drawing));
|
||||
const front = m.blocks.filter((b) => b.row === '전열')[0];
|
||||
const back = m.blocks.filter((b) => b.row === '후열')[0];
|
||||
expect(Math.round((front.top - front.bottom) * 1000)).toBe(164);
|
||||
expect(Math.round((back.top - back.bottom) * 1000)).toBe(139);
|
||||
});
|
||||
|
||||
it('a drawing row that is off by more than 5 mm is flagged', () => {
|
||||
const bad = drawing.map((d) => (d.no === 3 ? { ...d, elx: d.elx + 0.02 } : d));
|
||||
const c = drawingCopingCheck(buildPier(loadP1()), bad).find((x) => x.no === 3)!;
|
||||
expect(c.ok).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -18,12 +18,8 @@ describe('교량받침 상세 평면 (승인 2026-10-26)', () => {
|
||||
expect(bearingPlanSvg(integrated, 0)).toContain('width="2500" height="1200"');
|
||||
});
|
||||
|
||||
it('the drawing values are overlaid as dashed outlines only when given', () => {
|
||||
const plain = bearingPlanSvg(separate, 0);
|
||||
expect(plain).not.toContain('stroke-dasharray="40 25"');
|
||||
const overlay = { front: { A: 700, B: 640 }, back: { A: 700, B: 540 } };
|
||||
const svg = bearingPlanSvg(separate, 0, overlay);
|
||||
expect((svg.match(/stroke-dasharray="40 25"/g) ?? []).length).toBe(2);
|
||||
it('draws the solplate of each bearing from the pier plan (L 1000 across, W 1100 along)', () => {
|
||||
expect((bearingPlanSvg(separate, 0).match(/width="1000" height="1100"/g) ?? []).length).toBe(2);
|
||||
});
|
||||
|
||||
it('an unknown pair index gives an empty drawing', () => {
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { BEARING_ELEVATION_REF, SOLPLATE_REF, SOLPLATE_T_RULE, solplateTs } from '../src/core/bearing-ref';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
describe('교량받침 reference (도면값, 읽기 전용, 1단계)', () => {
|
||||
it('E = h1 + h2 + h3 + h4 on every row', () => {
|
||||
for (const r of BEARING_ELEVATION_REF) expect(r.h1 + r.h2 + r.h3 + r.h4, `No ${r.no}`).toBe(r.E);
|
||||
});
|
||||
|
||||
it('solplate L = A + 100 and W = B + 100 against the elevation row', () => {
|
||||
BEARING_ELEVATION_REF.forEach((e, i) => {
|
||||
expect(SOLPLATE_REF[i].L, `No ${e.no}`).toBe(e.A + 100);
|
||||
expect(SOLPLATE_REF[i].W, `No ${e.no}`).toBe(e.B + 100);
|
||||
});
|
||||
});
|
||||
|
||||
it('T1 and T2 follow T ± L·|S|/2 on every row (the 검산 column)', () => {
|
||||
for (const r of SOLPLATE_REF) {
|
||||
const c = solplateTs(SOLPLATE_T_RULE, r.L, r.s);
|
||||
expect(c.T1, `No ${r.no}`).toBe(r.T1);
|
||||
expect(c.T2, `No ${r.no}`).toBe(r.T2);
|
||||
}
|
||||
});
|
||||
|
||||
it('the solplate tapers along the axis: T1 (−측) + T2 (+측) = 2·T, with T = 30 at the centre', () => {
|
||||
for (const r of SOLPLATE_REF) {
|
||||
expect(r.T, `No ${r.no}`).toBe(SOLPLATE_T_RULE);
|
||||
expect(r.T1 + r.T2, `No ${r.no}`).toBe(2 * r.T);
|
||||
expect(r.T1, `No ${r.no}`).toBeGreaterThan(r.T2);
|
||||
}
|
||||
});
|
||||
|
||||
it('Excel z is EL.X (option 1): the model keeps z; the drawing EL.X minus Excel z is +64 mm (전열) and −11 mm (후열)', () => {
|
||||
const m = buildPier(loadP1());
|
||||
const back = m.blocks.filter((b) => b.row === '후열');
|
||||
const front = m.blocks.filter((b) => b.row === '전열');
|
||||
BEARING_ELEVATION_REF.forEach((r) => {
|
||||
const z = r.row === '전열' ? front[r.no - 1].padTop : back[r.no - 6].padTop;
|
||||
const diffMm = (r.elx - z) * 1000; // 도면 − 엑셀
|
||||
// About +64 mm on the front row and −11 mm on the back row (varies by a millimetre or two per bearing).
|
||||
expect(Math.abs(diffMm - (r.row === '전열' ? 64 : -11)), `No ${r.no}`).toBeLessThan(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
import { applyBearingRules, BEARING_RULES_DEFAULT } from '../src/core/bearing-ref';
|
||||
|
||||
describe('교량받침 공통 규칙 (2단계)', () => {
|
||||
it('with the drawing values, the rules give back the drawing E, L, W, T1, T2 on every bearing', () => {
|
||||
BEARING_ELEVATION_REF.forEach((r, i) => {
|
||||
const v = applyBearingRules(r, BEARING_RULES_DEFAULT);
|
||||
expect(v.E, `No ${r.no}`).toBe(r.E);
|
||||
expect(v.L, `No ${r.no}`).toBe(SOLPLATE_REF[i].L);
|
||||
expect(v.W, `No ${r.no}`).toBe(SOLPLATE_REF[i].W);
|
||||
expect(v.T1, `No ${r.no}`).toBe(SOLPLATE_REF[i].T1);
|
||||
expect(v.T2, `No ${r.no}`).toBe(SOLPLATE_REF[i].T2);
|
||||
});
|
||||
});
|
||||
|
||||
it('a 10 mm larger mortar h2 raises E by 10 and leaves L, W and T1, T2 unchanged', () => {
|
||||
const r = BEARING_ELEVATION_REF[0];
|
||||
const v = applyBearingRules(r, { ...BEARING_RULES_DEFAULT, h2: BEARING_RULES_DEFAULT.h2 + 10 });
|
||||
expect(v.E).toBe(r.E + 10);
|
||||
expect(v.L).toBe(r.A + 100);
|
||||
expect(v.T1).toBe(SOLPLATE_REF[0].T1);
|
||||
});
|
||||
});
|
||||
@@ -6,21 +6,14 @@ import { loadP1 } from './helpers';
|
||||
|
||||
const model = buildPier(loadP1());
|
||||
const integrated = buildPier({ ...loadP1(), bearingLayout: '일체' });
|
||||
const ref = { h1: 150, h2: 50, h3: 181, h4: 30, E: 425, A: 700, B: 640 };
|
||||
|
||||
describe('교량받침 상세 단면 (승인 2026-10-26)', () => {
|
||||
describe('교량받침 상세 단면 (승인 2026-10-26, C안 2026-11-03)', () => {
|
||||
it('labels EL.X at the mortar top and the block height h1 for each bearing', () => {
|
||||
const svg = bearingSectionSvg(model, 0);
|
||||
expect(svg).toContain('EL.X');
|
||||
expect(svg).toContain('h1 150');
|
||||
});
|
||||
|
||||
it('draws the drawing heights dashed only when given', () => {
|
||||
expect(bearingSectionSvg(model, 0)).not.toContain('stroke-dasharray="40 25"');
|
||||
const svg = bearingSectionSvg(model, 0, { front: ref, back: ref });
|
||||
expect((svg.match(/stroke-dasharray="40 25"/g) ?? []).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('일체 draws one stepped outline (a polygon) for the pair', () => {
|
||||
expect(bearingSectionSvg(integrated, 0)).toContain('<polygon');
|
||||
});
|
||||
@@ -30,15 +23,13 @@ describe('교량받침 상세 단면 (승인 2026-10-26)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('솔플레이트 상세 (승인 2026-10-26)', () => {
|
||||
it('shows L, W, T1 and T2 of the model plan, with the drawing dashed when given', () => {
|
||||
const withPlan = buildPier({ ...loadP1(), bearingPlans: [{ no: 1, A: 700, B: 640, S: -1.9 }] });
|
||||
const block = withPlan.blocks.find((b) => b.no === 1)!;
|
||||
describe('솔플레이트 상세 (승인 2026-10-26, C안 2026-11-03)', () => {
|
||||
it('shows L, W, T1 and T2 of the pier plan', () => {
|
||||
const block = model.blocks.find((b) => b.no === 1)!;
|
||||
const svg = solplateDetailSvg(block);
|
||||
expect(svg).toContain('L ');
|
||||
expect(svg).toContain('T1 ');
|
||||
expect(svg).toContain('T2 ');
|
||||
const withRef = solplateDetailSvg(block, { L: 800, W: 748, T1: 38, T2: 22, S: 1.9 });
|
||||
expect(withRef).toContain('stroke-dasharray="40 25"');
|
||||
expect(svg).toContain('L 1000');
|
||||
expect(svg).toContain('W 1100');
|
||||
expect(svg).toContain('T1 27.5');
|
||||
expect(svg).toContain('T2 22.5');
|
||||
});
|
||||
});
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseDrawingRows } from '../src/core/drawing-import';
|
||||
|
||||
const header = ['No', '열', 'EL.X (m)', 'h1 (mm)', 'h2 (mm)'];
|
||||
|
||||
describe('도면 불러오기 (승인 2026-10-23)', () => {
|
||||
it('reads No, EL.X, h1, h2 from a sheet with a header and ignores other columns', () => {
|
||||
const rows = [['도면 받침', '', '', '', ''], header, [1, '전열', 43.625, 164, 50], [2, '전열', 43.697, 164, 50]];
|
||||
const r = parseDrawingRows(rows);
|
||||
expect(r.errors).toEqual([]);
|
||||
expect(r.rows).toEqual([
|
||||
{ no: 1, elx: 43.625, h1: 164, h2: 50 },
|
||||
{ no: 2, elx: 43.697, h1: 164, h2: 50 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('accepts the header spelled as "EL. X" and in a different order', () => {
|
||||
const rows = [['h2', 'No', 'h1', 'EL. X'], [50, 1, 164, 43.625]];
|
||||
const r = parseDrawingRows(rows);
|
||||
expect(r.errors).toEqual([]);
|
||||
expect(r.rows[0]).toEqual({ no: 1, elx: 43.625, h1: 164, h2: 50 });
|
||||
});
|
||||
|
||||
it('reports a missing header, a duplicate No, and an EL.X given in mm', () => {
|
||||
expect(parseDrawingRows([['x', 'y']]).errors[0]).toContain('머리글');
|
||||
const dup = parseDrawingRows([header, [1, '', 43.6, 164, 50], [1, '', 43.7, 164, 50]]);
|
||||
expect(dup.errors.some((e) => e.includes('중복'))).toBe(true);
|
||||
const mm = parseDrawingRows([header, [1, '', 43625, 164, 50]]);
|
||||
expect(mm.errors.some((e) => e.includes('m 단위'))).toBe(true);
|
||||
expect(mm.rows).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -1,70 +0,0 @@
|
||||
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('a leader-line "EL. X" label placed before the table does not replace the table header', () => {
|
||||
const distractor = { x: 11102.8, y: 11521.7, text: 'EL. "X"' };
|
||||
const r = parseBearingElevationTable([distractor, ...texts]);
|
||||
expect(r.errors).toEqual([]);
|
||||
expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
});
|
||||
|
||||
it('a stray "NO" label elsewhere on the drawing does not move the No column', () => {
|
||||
const r = parseBearingElevationTable([{ x: 100, y: 100, text: 'NO.' }, ...texts]);
|
||||
expect(r.errors).toEqual([]);
|
||||
expect(r.drawing.map((d) => d.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
});
|
||||
|
||||
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"' }]);
|
||||
});
|
||||
});
|
||||
|
||||
import { parseSolplateTable } from '../src/core/dxf-table';
|
||||
import { SOLPLATE_REF } from '../src/core/bearing-ref';
|
||||
|
||||
describe('솔플레이트 TABLE 읽기 (승인 2026-10-23)', () => {
|
||||
const sp = JSON.parse(readFileSync(new URL('./fixtures/solplate-table-texts.json', import.meta.url), 'utf-8')) as DxfText[];
|
||||
|
||||
it('a stray "S(%)" label elsewhere on the drawing does not replace the solplate header', () => {
|
||||
const r = parseSolplateTable([{ x: 16000, y: 16200, text: 'S(%)' }, ...sp]);
|
||||
expect(r.errors).toEqual([]);
|
||||
expect(r.rows.length).toBe(10);
|
||||
});
|
||||
|
||||
it('reads S, L, W, T, T1 and T2 for all ten bearings, and they equal the reference values', () => {
|
||||
const r = parseSolplateTable(sp);
|
||||
expect(r.errors).toEqual([]);
|
||||
expect(r.rows.map((x) => x.no)).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
for (const ref of SOLPLATE_REF) {
|
||||
const got = r.rows.find((x) => x.no === ref.no)!;
|
||||
expect(got.S, `No ${ref.no} S`).toBeCloseTo(ref.s, 3);
|
||||
expect(got.L, `No ${ref.no} L`).toBe(ref.L);
|
||||
expect(got.W, `No ${ref.no} W`).toBe(ref.W);
|
||||
expect(got.T, `No ${ref.no} T`).toBe(30);
|
||||
expect(got.T1, `No ${ref.no} T1`).toBe(38);
|
||||
expect(got.T2, `No ${ref.no} T2`).toBe(22);
|
||||
}
|
||||
});
|
||||
});
|
||||
+2
-2
@@ -29,11 +29,11 @@ describe('3D frame (view/scene.ts)', () => {
|
||||
expect(mapped.distanceTo(expected)).toBeLessThan(1e-6);
|
||||
});
|
||||
|
||||
it('bounding box spans pile tip (2.208 m) to the highest pad top (back bearing 5, 43.898 m = sheet E41)', () => {
|
||||
it('bounding box spans pile tip (2.208 m) to the highest solplate top (back bearing 5: pad top 43.898 m + h3 0.186 m + T1 0.0275 m)', () => {
|
||||
const scene = buildPierScene(model);
|
||||
const box = new THREE.Box3().setFromObject(scene.root);
|
||||
expect(box.min.y).toBeCloseTo(2.208, 2);
|
||||
expect(box.max.y).toBeCloseTo(43.898, 2);
|
||||
expect(box.max.y).toBeCloseTo(43.898 + 0.186 + 0.0275, 3);
|
||||
});
|
||||
|
||||
it('builds the expected part counts for P1', () => {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SOLPLATE_RULE_DEFAULT, solplateCenterT, solplatePlan, solplateRuleIssues } from '../src/core/solplate-rule';
|
||||
|
||||
describe('솔플레이트 규칙 (C안, 2026-11-03)', () => {
|
||||
it('default 211 · 1000 × 1100 · −0.5 %: T = 25, T1 = 27.5, T2 = 22.5, h3 = 211 − 25 = 186', () => {
|
||||
const p = solplatePlan(SOLPLATE_RULE_DEFAULT);
|
||||
expect(p.T).toBe(25);
|
||||
expect(p.T1).toBeCloseTo(27.5, 9);
|
||||
expect(p.T2).toBeCloseTo(22.5, 9);
|
||||
expect(p.h3).toBe(186);
|
||||
expect(p.L).toBe(1000);
|
||||
expect(p.W).toBe(1100);
|
||||
});
|
||||
|
||||
it('the centre thickness is the smallest whole mm that keeps T2 at 22 mm or more', () => {
|
||||
for (const [L, S] of [[1000, -0.5], [1100, -0.5], [1000, -2], [800, -1.91], [1200, -3]] as const) {
|
||||
const T = solplateCenterT(L, S);
|
||||
expect(T - (L * Math.abs(S)) / 200).toBeGreaterThanOrEqual(22);
|
||||
expect(solplateCenterT(L, S) - 1 - (L * Math.abs(S)) / 200).toBeLessThan(22);
|
||||
}
|
||||
});
|
||||
|
||||
it('T1 + T2 = 2T for any rule', () => {
|
||||
const p = solplatePlan({ heightMm: 220, lengthMm: 900, widthMm: 800, slopePct: -1.2 });
|
||||
expect(p.T1 + p.T2).toBeCloseTo(2 * p.T, 9);
|
||||
});
|
||||
|
||||
it('the default rule has no input issues', () => {
|
||||
expect(solplateRuleIssues(SOLPLATE_RULE_DEFAULT)).toEqual([]);
|
||||
});
|
||||
|
||||
it('reports a bearing height that does not exceed the solplate centre thickness', () => {
|
||||
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, heightMm: 25 })).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('reports non-positive plan sizes and a non-numeric slope', () => {
|
||||
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, lengthMm: 0 }).length).toBeGreaterThan(0);
|
||||
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, widthMm: -1 }).length).toBeGreaterThan(0);
|
||||
expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, slopePct: NaN }).length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -2,44 +2,38 @@ import { describe, expect, it } from 'vitest';
|
||||
import * as THREE from 'three';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { solplateGeometry } from '../src/core/solplate';
|
||||
import { BEARING_ELEVATION_REF, SOLPLATE_REF } from '../src/core/bearing-ref';
|
||||
import { buildPierScene } from '../src/view/scene';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
const plans = SOLPLATE_REF.map((s, i) => ({ no: s.no, A: BEARING_ELEVATION_REF[i].A, B: BEARING_ELEVATION_REF[i].B, S: s.s }));
|
||||
|
||||
describe('솔플레이트 모델과 3D (2단계, 승인 2026-10-23)', () => {
|
||||
it('without plans there is no solplate; with plans every bearing has one', () => {
|
||||
expect(buildPier(loadP1()).blocks.every((b) => b.plan === undefined)).toBe(true);
|
||||
const m = buildPier({ ...loadP1(), bearingPlans: plans, solplate: { T: 30, margin: 100, h3: 181 } });
|
||||
expect(m.blocks.every((b) => b.plan !== undefined)).toBe(true);
|
||||
expect(m.blocks[0].plan!.A).toBe(700);
|
||||
describe('솔플레이트 모델과 3D (C안, 2026-11-03)', () => {
|
||||
it('every pier has a solplate plan from its rule; the default gives T 25 and h3 186', () => {
|
||||
const m = buildPier(loadP1());
|
||||
expect(m.blocks.every((b) => b.plan.T === 25 && b.plan.h3 === 186)).toBe(true);
|
||||
});
|
||||
|
||||
it('the 3D scene has one solplate wedge per bearing, and its height is the bearing top plus T', () => {
|
||||
const m = buildPier({ ...loadP1(), bearingPlans: plans, solplate: { T: 30, margin: 100, h3: 181 } });
|
||||
it('a pier rule changes its own plan: 받침높이 211 and slope −1 % give T 27 and h3 184', () => {
|
||||
const m = buildPier({ ...loadP1(), solplate: { heightMm: 211, lengthMm: 1000, widthMm: 1100, slopePct: -1 } });
|
||||
expect(m.blocks[0].plan.T).toBe(27);
|
||||
expect(m.blocks[0].plan.h3).toBe(184);
|
||||
});
|
||||
|
||||
it('the 3D scene has one solplate wedge per bearing, and its highest point is the thick end of the tallest one', () => {
|
||||
const m = buildPier(loadP1());
|
||||
const scene = buildPierScene(m);
|
||||
const wedges = scene.parts.solplates.children;
|
||||
expect(wedges.length).toBe(10);
|
||||
const box = new THREE.Box3().setFromObject(scene.parts.solplates);
|
||||
// The highest point is the thick (시점) end of the tallest wedge: underside (block top + h3) + T1.
|
||||
const top = Math.max(...m.blocks.map((b) => solplateGeometry(b, 181, { A: b.plan!.A, B: b.plan!.B, margin: 100, T: 30, slopePct: b.plan!.S }).bottom + solplateGeometry(b, 181, { A: b.plan!.A, B: b.plan!.B, margin: 100, T: 30, slopePct: b.plan!.S }).T1));
|
||||
const top = Math.max(...m.blocks.map((b) => solplateGeometry(b, b.plan).bottom + solplateGeometry(b, b.plan).T1));
|
||||
expect(box.max.y).toBeCloseTo(top, 3);
|
||||
});
|
||||
|
||||
it('without plans the scene has an empty solplate part', () => {
|
||||
it('the 교량받침 본체 is one box per bearing: the plan less 100 mm on each side, height h3 (186 mm)', () => {
|
||||
const scene = buildPierScene(buildPier(loadP1()));
|
||||
expect(scene.parts.solplates.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('the 교량받침 본체 is one box per bearing: the solplate plan less 100 mm on each side, height h3 (181 mm)', () => {
|
||||
const m = buildPier({ ...loadP1(), bearingPlans: plans, solplate: { T: 30, margin: 100, h3: 181 } });
|
||||
const scene = buildPierScene(m);
|
||||
const bodies = scene.parts.bearings.children;
|
||||
expect(bodies.length).toBe(10);
|
||||
const one = new THREE.Box3().setFromObject(bodies[0]);
|
||||
expect(one.max.y - one.min.y).toBeCloseTo(0.181, 3);
|
||||
// Body 1: L = 800 − 200 = 600 across the 교각축 직각, W = 740 − 200 = 540 along the 교축; the row skew turns it a few degrees.
|
||||
expect(Math.max(one.max.x - one.min.x, one.max.z - one.min.z)).toBeGreaterThan(0.54);
|
||||
expect(one.max.y - one.min.y).toBeCloseTo(0.186, 3);
|
||||
// Body: L − 200 = 800 across the 교각축 직각, W − 200 = 900 along the 교축; the row skew turns it a few degrees.
|
||||
expect(Math.max(one.max.x - one.min.x, one.max.z - one.min.z)).toBeGreaterThan(0.85);
|
||||
});
|
||||
});
|
||||
+19
-18
@@ -1,41 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { buildPier } from '../src/core/geometry';
|
||||
import { solplateGeometry } from '../src/core/solplate';
|
||||
import { SOLPLATE_RULE_DEFAULT, solplatePlan } from '../src/core/solplate-rule';
|
||||
import { loadP1 } from './helpers';
|
||||
|
||||
const model = buildPier(loadP1());
|
||||
const front1 = model.blocks.filter((b) => b.row === '전열')[0];
|
||||
const params = { A: 700, B: 640, margin: 100, T: 30, slopePct: -1.91 };
|
||||
const plan = solplatePlan(SOLPLATE_RULE_DEFAULT);
|
||||
|
||||
describe('솔플레이트 기하 (1단계, 승인 2026-10-23)', () => {
|
||||
it('L = A + 100 and W = B + 100 along the 교축 and the 교각축 직각', () => {
|
||||
const g = solplateGeometry(front1, 181, params);
|
||||
expect(g.L).toBeCloseTo(0.8, 6);
|
||||
expect(g.W).toBeCloseTo(0.74, 6);
|
||||
describe('솔플레이트 기하 (C안, 2026-11-03)', () => {
|
||||
it('L = 교직 1000 across the 교각축 직각 (n) and W = 교축 1100 along the 교축 (s)', () => {
|
||||
const g = solplateGeometry(front1, plan);
|
||||
expect(g.L).toBeCloseTo(1.0, 6);
|
||||
expect(g.W).toBeCloseTo(1.1, 6);
|
||||
});
|
||||
|
||||
it('the thick end T1 is on the 시점 side (−s): T1 ≈ 38, T2 ≈ 22 mm, and T1 + T2 = 2T', () => {
|
||||
const g = solplateGeometry(front1, 181, params);
|
||||
expect(g.T1 * 1000).toBeCloseTo(37.64, 1);
|
||||
expect(g.T2 * 1000).toBeCloseTo(22.36, 1);
|
||||
expect((g.T1 + g.T2) * 1000).toBeCloseTo(60, 6);
|
||||
// The top at the 시점 corners is the thick side.
|
||||
it('the thick end T1 is on the (−) side: T1 = 27.5 mm, T2 = 22.5 mm, and T1 + T2 = 2T', () => {
|
||||
const g = solplateGeometry(front1, plan);
|
||||
expect(g.T1 * 1000).toBeCloseTo(27.5, 6);
|
||||
expect(g.T2 * 1000).toBeCloseTo(22.5, 6);
|
||||
expect((g.T1 + g.T2) * 1000).toBeCloseTo(50, 6);
|
||||
// The top at the (−) corners is the thick side.
|
||||
const topAtStart = g.corners.filter((c) => Math.abs(c[1] - (g.bottom + g.T1)) < 1e-9);
|
||||
expect(topAtStart.length).toBe(2);
|
||||
});
|
||||
|
||||
it('the underside is the bearing top: block top (EL.X) + h3', () => {
|
||||
const g = solplateGeometry(front1, 181, params);
|
||||
expect(g.bottom).toBeCloseTo(front1.padTop + 0.181, 6);
|
||||
it('the underside is the bearing top: block top (EL.X) + h3 (186 mm)', () => {
|
||||
const g = solplateGeometry(front1, plan);
|
||||
expect(g.bottom).toBeCloseTo(front1.padTop + 0.186, 6);
|
||||
});
|
||||
|
||||
it('volume = L·W·T (the taper averages to the centre thickness)', () => {
|
||||
const g = solplateGeometry(front1, 181, params);
|
||||
expect(g.volume).toBeCloseTo(g.L * g.W * 0.03, 9);
|
||||
const g = solplateGeometry(front1, plan);
|
||||
expect(g.volume).toBeCloseTo(g.L * g.W * 0.025, 9);
|
||||
});
|
||||
|
||||
it('the solplate is centred on the block and follows the row direction', () => {
|
||||
const g = solplateGeometry(front1, 181, params);
|
||||
const g = solplateGeometry(front1, plan);
|
||||
const c = g.corners.reduce((acc, p) => [acc[0] + p[0] / 8, acc[1] + p[2] / 8] as [number, number], [0, 0]);
|
||||
expect(c[0]).toBeCloseTo(front1.s, 6);
|
||||
expect(c[1]).toBeCloseTo(front1.n, 6);
|
||||
|
||||
Reference in new issue
Block a user