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>`;
|
||||
|
||||
Reference in new issue
Block a user