feat(bearing): 교각 솔플레이트를 2026-11-06 결정으로 (짝 받침 S · 열 T · 거더 하면 고정)
받침방위각_솔플레이트검토.md §3과 시안(Version 110–113)을 따른다 (사용자 결정 2026-11-06: 시안 적용). 11-03 C안(받침높이 H 고정, h3 = H − T, S 입력)을 대체한다. - S: 교량 전체(A1, P1 … Pn, A2) 거더 짝 받침의 z 차 / 수평거리. 없으면 교각 종단경사 입력 → 다른 열 같은 순번 값(가정, WS03) → 0 %(WS03). 받침별 직접 수정(수정됨) - 평면 L = A + 100, W = B + 100 (받침별 A × B, 기본 700 × 640 가정), h3 181 - T = 열마다 max(22, ⌈22 + L·|S|/2⌉), 열 T 직접 수정 가능(T2 < T_min → WS02). T1은 오르막 쪽(3D 쐐기의 두꺼운 끝도 그쪽), T2 반대쪽 - 거더 하면 고정: 블럭 상면 = z − (T − T_ref), T_ref 30 - 짝 받침은 불러오기·프로젝트 열기 때 교량 전체로 계산해 교각 생성에 넘김 - ② 카드: h3·T_ref·T_min·여유·열 T·종단경사 입력, 표에 짝 받침·S 근거·받침별 S/A/B 수정·검산 W08·검사 - P1 검증(A1과 함께): S −1.908 … −1.902 %, L 800 · W 740 · T 30 · T1 38 · T2 22, 블럭 높이 변화 없음 - 블럭·솔플레이트 테스트는 A1–P1 짝 문맥으로 실행 (짝이 없으면 S 0 · T 22 · 블럭 +8 mm) 교대(QA-15 C안)와 Pier 시트 종단경사 열 읽기는 아직 남음. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5fec2743cb
commit
a0b0929e13
19 files changed
+486
-109
No files matched your search
+111
-39
@@ -119,8 +119,10 @@ 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 { SOLPLATE_MIN_T2_MM, SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule';
|
||||
import type { SolplateRule } from './core/types';
|
||||
import { SOLE_DEFAULTS, type SoleSettings, soleSettingsIssues } from './core/sole-plate';
|
||||
import { SOLPLATE_MIN_T2_MM } from './core/solplate-rule';
|
||||
import { bridgePairs } from './core/bridge-pairs';
|
||||
import type { BearingPair } from './core/bearing-pair';
|
||||
import { type BarSummary, elevationRows, solplateRows } from './core/bearing-table';
|
||||
|
||||
/** 지반 모델 (ground_model.json) for the project. Its values reach a pier's 지반고 only when the user applies them. */
|
||||
@@ -254,6 +256,8 @@ const state = {
|
||||
pending: null as Pending | null,
|
||||
source: { fileName: '', sheets: [] as string[] },
|
||||
history: [] as ProjectFile['history'],
|
||||
/** Girder pairs over the bridge (A1, P1 … Pn, A2) for the solplate S (2026-11-06); recomputed when structures load. */
|
||||
bridgePairs: [] as BearingPair[],
|
||||
/** Snapshot of the inputs at the last save or open. null until the project is first saved. */
|
||||
savedSnapshot: null as string | null,
|
||||
};
|
||||
@@ -396,12 +400,12 @@ function buildEntry(rawInput: PierInput, checked: boolean): Pier {
|
||||
const input = withCopingDefaults(rawInput);
|
||||
const issues: Issue[] = [];
|
||||
let model: PierModel | null = null;
|
||||
// 솔플레이트 규칙 (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);
|
||||
// 솔플레이트 입력 (2026-11-06): settings that cannot make a solplate are reported, and no model is built from them.
|
||||
const solIssues = soleSettingsIssues(input.sole);
|
||||
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(input);
|
||||
model = buildPier(input, { pairs: state.bridgePairs });
|
||||
validatePier(input, model, issues);
|
||||
for (const message of columnDiameterIssues(input.columnDiameterMm ?? 2000, model)) {
|
||||
issues.push({ code: 'D01', level: 'E', pier: input.name, message });
|
||||
@@ -5116,20 +5120,50 @@ function renderElevationTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
}
|
||||
|
||||
/** 솔플레이트 TABLE (모델값, mm, 2026-11-03): S, L, W, T, T1, T2 and the check T1 − T2 ≈ L·|S|/100. */
|
||||
/**
|
||||
* 솔플레이트 TABLE (모델값, mm, 2026-11-06): 짝 받침, S와 근거, A × B, L, W, T, T1 (오르막 쪽), T2, 검산 W08, 검사.
|
||||
* S, A and B can be typed per bearing (수정됨); clearing the cell goes back to the computed or default value.
|
||||
*/
|
||||
function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||
if (!pier.model) return;
|
||||
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm)', 'lbl'));
|
||||
wrap.appendChild(el('p', '솔플레이트 TABLE (모델값 · mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl'));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const head = el('thead');
|
||||
const hr = el('tr');
|
||||
for (const h of ['No', 'S (%)', 'L (교직)', 'W (교축)', 'T (중심)', 'T1 (−측)', 'T2 (+측)', '검산 T1 − T2 ≈ L·|S|/100']) hr.appendChild(el('th', h));
|
||||
for (const h of ['No', '짝 받침', 'S (%)', 'S 근거', 'A', 'B', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사']) hr.appendChild(el('th', h));
|
||||
head.appendChild(hr);
|
||||
table.appendChild(head);
|
||||
const body = el('tbody');
|
||||
const settings = pier.draft.sole ?? {};
|
||||
const cellInput = (value: number, typed: boolean, label: string, apply: (v: number | undefined) => void) => {
|
||||
const input = el('input');
|
||||
input.type = 'number'; input.step = 'any'; input.value = String(value); input.setAttribute('aria-label', label);
|
||||
input.style.width = '6.5em';
|
||||
if (typed) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => apply(input.value.trim() === '' ? undefined : Number(input.value)));
|
||||
const td = el('td'); td.appendChild(input); return td;
|
||||
};
|
||||
for (const r of solplateRows(pier.model)) {
|
||||
const key = String(r.no);
|
||||
const tr = el('tr');
|
||||
const cells = [String(r.no), r.S.toFixed(2), String(r.L), String(r.W), String(r.T), r.T1.toFixed(1), r.T2.toFixed(1), `${r.checkLhs.toFixed(1)} ≈ ${r.checkRhs.toFixed(1)} ${r.ok ? '일치' : '불일치'}`];
|
||||
tr.append(el('td', String(r.no)), el('td', r.pair ?? '없음'));
|
||||
tr.appendChild(cellInput(Math.round(r.S * 1000) / 1000, r.source === '수정됨', `받침 ${r.no} S (%)`, (v) => {
|
||||
const next = { ...(settings.slopePct ?? {}) };
|
||||
if (v === undefined) delete next[key]; else next[key] = v;
|
||||
setSole(pier.input.name, { ...settings, slopePct: next });
|
||||
}));
|
||||
tr.appendChild(el('td', r.source));
|
||||
const plate = settings.plates?.[key];
|
||||
for (const dim of ['aMm', 'bMm'] as const) {
|
||||
tr.appendChild(cellInput(dim === 'aMm' ? r.A : r.B, plate !== undefined, `받침 ${r.no} ${dim === 'aMm' ? 'A' : 'B'} (mm)`, (v) => {
|
||||
const next = { ...(settings.plates ?? {}) };
|
||||
const cur = next[key] ?? { aMm: r.A, bMm: r.B };
|
||||
if (v === undefined) delete next[key]; else next[key] = { ...cur, [dim]: v };
|
||||
setSole(pier.input.name, { ...settings, plates: next });
|
||||
}));
|
||||
}
|
||||
const cells = [String(r.L), String(r.W), `${r.T}${r.tTyped ? ' (수정됨)' : ''}`, String(r.T1), String(r.T2),
|
||||
`${r.checkLhs} ≈ ${r.checkRhs.toFixed(1)} ${r.ok ? '일치' : '불일치 (W08)'}`, r.warnings.length ? r.warnings.join(' · ') : '—'];
|
||||
for (const c of cells) tr.appendChild(el('td', c));
|
||||
body.appendChild(tr);
|
||||
}
|
||||
@@ -5137,47 +5171,79 @@ function renderSolplateTable(wrap: HTMLDivElement, pier: Pier): void {
|
||||
wrap.appendChild(el('div', undefined, 'table-scroll')).appendChild(table);
|
||||
}
|
||||
|
||||
/** Sets one pier's solplate rule (C안, 2026-11-03) and rebuilds that pier. */
|
||||
function setSolplateRule(name: string, rule: SolplateRule): void {
|
||||
/** Sets one pier's solplate settings (2026-11-06) and rebuilds that pier. */
|
||||
function setSole(name: string, sole: SoleSettings): void {
|
||||
const p = state.piers.find((q) => q.input.name === name);
|
||||
if (!p) return;
|
||||
p.draft = { ...p.draft, solplate: rule };
|
||||
const issues = soleSettingsIssues(sole);
|
||||
if (issues.length) { setError(issues.join(' · ')); return; }
|
||||
p.draft = { ...p.draft, sole };
|
||||
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
|
||||
setupScene();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/** 솔플레이트 규칙 (C안, 2026-11-03): per pier. The plan L × W and the slope S are inputs; 받침높이 H is fixed (211 mm, not on the UI). T and h3 follow. */
|
||||
/**
|
||||
* 솔플레이트 입력 (결정 2026-11-06, 시안): h3, T_ref, T_min, 여유 (L − A = W − B), 열마다 T (비우면 자동), 교각 종단경사
|
||||
* (짝 받침이 없을 때). 거더 하면을 고정하므로 T가 T_ref와 다르면 블럭 상면 = z − (T − T_ref).
|
||||
*/
|
||||
function renderSolplateRule(wrap: HTMLDivElement, pier: Pier): void {
|
||||
const rule = pier.draft.solplate ?? SOLPLATE_RULE_DEFAULT;
|
||||
wrap.appendChild(el('p', `솔플레이트 규칙 (이 교각 · 기본 1000 × 1100 · −0.5 %) · 받침높이 H ${rule.heightMm} mm 고정 (솔플레이트 포함)`, 'lbl'));
|
||||
const fields: [keyof SolplateRule, string][] = [
|
||||
['lengthMm', '솔플레이트 교직 L (mm)'],
|
||||
['widthMm', '솔플레이트 교축 W (mm)'],
|
||||
['slopePct', '종단 기울기 S (%)'],
|
||||
];
|
||||
const sole = pier.draft.sole ?? {};
|
||||
wrap.appendChild(el('p', `솔플레이트 (결정 2026-11-06) · T = 열마다 max(${sole.tMinMm ?? SOLE_DEFAULTS.tMinMm}, ⌈${sole.tMinMm ?? SOLE_DEFAULTS.tMinMm} + L·|S|/2⌉) · 거더 하면 고정`, 'lbl'));
|
||||
const grid = el('div', undefined, 'edit-row');
|
||||
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(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'));
|
||||
const field = (id: string, label: string, value: number | undefined, base: string, apply: (v: number | undefined) => void) => {
|
||||
const lab = el('label', label); lab.htmlFor = id;
|
||||
const input = el('input'); input.id = id; input.type = 'number'; input.step = 'any';
|
||||
input.value = value === undefined ? '' : String(value);
|
||||
input.placeholder = base;
|
||||
if (value !== undefined) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => apply(input.value.trim() === '' ? undefined : Number(input.value)));
|
||||
grid.append(lab, input, el('span', `기본 ${base}`, 'muted small'));
|
||||
};
|
||||
const set = (patch: Partial<SoleSettings>) => setSole(pier.input.name, { ...sole, ...patch });
|
||||
field('sole-h3', '받침 높이 h3 (mm)', sole.h3Mm, String(SOLE_DEFAULTS.h3Mm), (v) => set({ h3Mm: v }));
|
||||
field('sole-tref', 'T_ref · 엑셀 z 산정 때 두께 (mm)', sole.tRefMm, String(SOLE_DEFAULTS.tRefMm), (v) => set({ tRefMm: v }));
|
||||
field('sole-tmin', '솔플 최소두께 T_min (mm)', sole.tMinMm, String(SOLE_DEFAULTS.tMinMm), (v) => set({ tMinMm: v }));
|
||||
field('sole-margin', '솔플 여유 L − A = W − B (mm)', sole.marginMm, String(SOLE_DEFAULTS.marginMm), (v) => set({ marginMm: v }));
|
||||
const rows = [...new Set(pier.input.bearings.map((b) => b.row))];
|
||||
for (const row of rows) {
|
||||
const auto = pier.model?.soles.find((s) => s.row === row);
|
||||
field(`sole-t-${row}`, `솔플레이트 T · ${row} (mm, 비우면 자동)`, sole.tByRow?.[row], auto && !auto.tTyped ? `자동 ${auto.T}` : '자동', (v) => {
|
||||
const next = { ...(sole.tByRow ?? {}) };
|
||||
if (v === undefined) delete next[row]; else next[row] = v;
|
||||
set({ tByRow: next });
|
||||
});
|
||||
}
|
||||
const lsId = 'sole-longitudinal';
|
||||
const lsLab = el('label', '교각 종단경사 (소수, 짝 받침이 없을 때)'); lsLab.htmlFor = lsId;
|
||||
const ls = el('input'); ls.id = lsId; ls.type = 'number'; ls.step = 'any';
|
||||
ls.value = pier.draft.longitudinalSlope === undefined ? '' : String(pier.draft.longitudinalSlope);
|
||||
ls.placeholder = '입력 없음';
|
||||
ls.addEventListener('change', () => {
|
||||
const p = state.piers.find((q) => q.input.name === pier.input.name);
|
||||
if (!p) return;
|
||||
const v = ls.value.trim() === '' ? undefined : Number(ls.value);
|
||||
if (v !== undefined && !Number.isFinite(v)) { setError('종단경사는 숫자여야 합니다.'); return; }
|
||||
p.draft = { ...p.draft, longitudinalSlope: v };
|
||||
state.piers = state.piers.map((q) => (q === p ? regenerateEntry(q) : q));
|
||||
setupScene();
|
||||
renderAll();
|
||||
});
|
||||
grid.append(lsLab, ls, el('span', '(−) = 교량진행방향으로 낮아짐 · Pier 시트 새 열(Q-SH2)', 'muted small'));
|
||||
wrap.appendChild(grid);
|
||||
const issues = solplateRuleIssues(rule);
|
||||
if (issues.length > 0) {
|
||||
wrap.appendChild(el('p', issues.join(' '), 'warn-text'));
|
||||
return;
|
||||
}
|
||||
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'));
|
||||
const m = pier.model;
|
||||
if (!m) return;
|
||||
const unpaired = m.soles.filter((s) => s.warnings.includes('WS03')).map((s) => s.no);
|
||||
const notes = [
|
||||
`받침 A × B 기본 ${SOLE_DEFAULTS.aMm} × ${SOLE_DEFAULTS.bMm} (P1 전열 값, 가정) · 표에서 받침별 수정`,
|
||||
unpaired.length ? `WS03 짝 없음: ${unpaired.join(', ')}번 → ${m.soles.find((s) => s.no === unpaired[0])?.slopeSource === '입력' ? '종단경사 입력' : '다른 열 값 또는 0 %'} 사용` : '모든 받침이 짝 받침으로 S 계산',
|
||||
...['WS01', 'WS02', 'WS04'].flatMap((w) => {
|
||||
const hit = m.soles.filter((s) => s.warnings.includes(w as 'WS01')).map((s) => s.no);
|
||||
return hit.length ? [`${w}: ${hit.join(', ')}번`] : [];
|
||||
}),
|
||||
...rows.map((row) => { const s = m.soles.find((x) => x.row === row)!; return `${row} 블럭 상면 보정 ${s.dropMm === 0 ? '0' : `${-s.dropMm > 0 ? '+' : ''}${-s.dropMm} mm`}`; }),
|
||||
];
|
||||
wrap.appendChild(el('p', `검사 · ${notes.join(' · ')}`, 'muted small'));
|
||||
}
|
||||
|
||||
/** ② 코핑 상세 (2단계, 읽기 전용): d_IP2 and the 계단 구간 표 from the model (승인 2026-10-23). */
|
||||
@@ -5512,6 +5578,7 @@ function openProjectText(text: string, saved: boolean): void {
|
||||
groundModel = parsed.groundModel ?? null;
|
||||
groundMapMode = groundModel?.mapLayers?.length ? 'satellite' : 'none';
|
||||
setGroundMapMode(groundMapMode);
|
||||
state.bridgePairs = bridgePairs(parsed.piers.map((p) => p.draft ?? p.input), state.abutments);
|
||||
state.piers = parsed.piers.map((p) => ({
|
||||
...buildEntry(p.input, false),
|
||||
defaults: p.defaults ?? p.input,
|
||||
@@ -5752,6 +5819,8 @@ function loadBytes(bytes: Uint8Array, fileName: string): void {
|
||||
const wb = XLSX.read(bytes, { type: 'array' });
|
||||
const { piers, sheets } = parseWorkbook(wb);
|
||||
const abut = parseAbutments(wb, piers.map((pier) => pier.name));
|
||||
// Solplate S needs the girder pairs of the whole file (2026-11-06); applyImport recomputes them for the chosen ones.
|
||||
state.bridgePairs = bridgePairs(piers, abut.abutments);
|
||||
const entries = piers.map((input) => buildEntry(input, false));
|
||||
// A sheet is selectable only if every pier in it builds. Show the reason next to the sheet.
|
||||
for (const sheet of sheets) {
|
||||
@@ -5794,6 +5863,9 @@ function applyImport(): void {
|
||||
state.piers = p.entries.filter((e) => e.checked && selectable(p, e));
|
||||
state.abutments = p.abutments.filter((rec) => p.selectedAbutments.has(rec.name) && selectableAbut(p, rec));
|
||||
state.abutDefaults = JSON.parse(JSON.stringify(state.abutments)) as AbutRecord[];
|
||||
// The chosen structures may differ from the whole file: rebuild with their girder pairs (솔플레이트 S, 2026-11-06).
|
||||
state.bridgePairs = bridgePairs(state.piers.map((e) => e.input), state.abutments);
|
||||
state.piers = state.piers.map((e) => ({ ...buildEntry(e.input, e.checked), defaults: e.defaults, draft: e.draft }));
|
||||
state.active = state.piers[0]?.input.name ?? null;
|
||||
state.filter = 'all';
|
||||
state.pending = null;
|
||||
|
||||
Reference in new issue
Block a user