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