diff --git a/src/core/abut/bearing-table.ts b/src/core/abut/bearing-table.ts index 872775c..4f67a15 100644 --- a/src/core/abut/bearing-table.ts +++ b/src/core/abut/bearing-table.ts @@ -1,27 +1,36 @@ /** * 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수. * - * - 솔플레이트는 교각과 같은 규칙(C안, src/core/solplate-rule.ts)을 쓴다: 받침높이 H(솔플레이트 포함)는 교대 받침 시트 값 - * (운곡교 A1 275 mm)으로 고정하고, 교직 L · 교축 W · 종단 기울기 S를 입력한다. 중심 두께 T = ⌈22 + L·|S|/200⌉, - * T1·T2 = T ± L·|S|/200, 받침 본체 h3 = H − T. 기본값은 교각과 같은 1000 × 1100 · −0.5 % (가정, QA-15, 결정 2026-11-03). - * - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 + h4 (= T) = h1 + h2 + H. z = 패드 상면 = EL.X. + * - 솔플레이트 (결정 2026-11-06, spec_abut §5·QA-15, src/core/sole-plate.ts): S = 같은 거더의 짝 받침(A1 ↔ P1 전열, + * A2 ↔ Pn)으로 계산, 짝이 없으면 교대 종단경사 i(엑셀 Abut). L = A + 100, W = B + 100 (A × B 기본 700 × 640, 교각 P1 + * 전열 값 가정). 받침이 1열이라 T는 교대 전체에 하나: max(22, ⌈22 + L·|S|/2⌉). 거더 하면 고정 → 블럭 상면 = z − (T − T_ref). + * - h3 기본값 = 받침 시트의 받침높이 − T_ref (A1 275 − 30 = 245): 엑셀 받침높이가 정한 거더 하면을 그대로 둔다 + * (시안은 교각 P1의 h3 181을 가정했으나 교대 시트에 받침높이가 있어 그 값을 지킨다). + * - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 + h4 (= T). z = 패드 상면 = EL.X. */ -import { solplatePlan, solplateRuleIssues } from '../solplate-rule'; -import type { SolplatePlan, SolplateRule } from '../types'; +import type { BearingPair } from '../bearing-pair'; +import { type BearingSole, SOLE_DEFAULTS, structureSoles } from '../sole-plate'; import { ABUT_PAD_MM, type AbutModel } from './geometry'; import type { AbutMembers } from './members'; +/** 교대 솔플레이트 입력 (2026-11-06). Every value optional; absent = the default. */ export interface AbutBearingRule { - /** 솔플레이트 교직 L (mm). */ - lengthMm: number; - /** 솔플레이트 교축 W (mm). */ - widthMm: number; - /** 종단 기울기 S (%). */ - slopePct: number; + /** 받침 본체 높이 h3 (mm). Absent = 받침높이 − T_ref. */ + h3Mm?: number; + tRefMm: number; + tMinMm: number; + /** 솔플 여유 L − A = W − B (mm). */ + marginMm: number; + /** 받침 평면 A (거더 방향) × B (mm), every bearing. */ + aMm: number; + bMm: number; + /** T typed (mm). Absent = 자동. */ + tMm?: number; + /** S typed for every bearing (%). Absent = 짝 받침 계산. */ + slopePct?: number; } -/** 기본값: 교각 솔플레이트 규칙과 같은 1000 × 1100 · −0.5 % (QA-15, 사용자 결정 2026-11-03). */ -export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { lengthMm: 1000, widthMm: 1100, slopePct: -0.5 }; +export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { tRefMm: SOLE_DEFAULTS.tRefMm, tMinMm: SOLE_DEFAULTS.tMinMm, marginMm: SOLE_DEFAULTS.marginMm, aMm: SOLE_DEFAULTS.aMm, bMm: SOLE_DEFAULTS.bMm }; /** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */ export const ABUT_BLOCK_MM = 1200; export const ABUT_PAD_PLAN_MM = 1000; @@ -30,17 +39,34 @@ const I01_MIN_MM = 50 + 13; const CIRCLED = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩']; -export function resolveBearingRule(changes?: Partial): AbutBearingRule { - return { ...ABUT_BEARING_RULE_DEFAULT, ...(changes ?? {}) }; +/** Keys the 2026-11-06 rule reads; values stored by the C안 (lengthMm, widthMm, kind) are ignored. */ +const RULE_KEYS = ['h3Mm', 'tRefMm', 'tMinMm', 'marginMm', 'aMm', 'bMm', 'tMm', 'slopePct'] as const; + +export function resolveBearingRule(changes?: Partial & Record): AbutBearingRule { + const own = Object.fromEntries(Object.entries(changes ?? {}).filter(([k]) => (RULE_KEYS as readonly string[]).includes(k))); + return { ...ABUT_BEARING_RULE_DEFAULT, ...own }; } -/** The pier rule with the 교대 받침높이 H (from the 받침 sheet). */ -export function abutSolplateRule(rule: AbutBearingRule, heightMm: number): SolplateRule { - return { heightMm, lengthMm: rule.lengthMm, widthMm: rule.widthMm, slopePct: rule.slopePct }; +/** Bridge context of the abutment card: the girder pairs and the 교대 종단경사 i (decimal). */ +export interface AbutSoleContext { + pairs?: BearingPair[]; + slopeI?: number; } -export function abutSolplatePlan(rule: AbutBearingRule, heightMm: number): SolplatePlan { - return solplatePlan(abutSolplateRule(rule, heightMm)); +/** Solplate of every bearing of the abutment (one row). */ +export function abutSoles(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): BearingSole[] { + const plates = Object.fromEntries(model.bearings.map((b) => [String(b.no), { aMm: rule.aMm, bMm: rule.bMm }])); + return structureSoles( + model.name, + model.bearings.map((b) => ({ no: b.no, row: '1열', azimuthDeg: b.azimuthDeg ?? 0 })), + { + h3Mm: rule.h3Mm ?? heightMm - rule.tRefMm, tRefMm: rule.tRefMm, tMinMm: rule.tMinMm, marginMm: rule.marginMm, plates, + tByRow: rule.tMm === undefined ? undefined : { '1열': rule.tMm }, + slopePct: rule.slopePct === undefined ? undefined : Object.fromEntries(model.bearings.map((b) => [String(b.no), rule.slopePct!])), + }, + ctx.pairs ?? [], + ctx.slopeI === undefined ? undefined : ctx.slopeI * 100, + ); } export interface AbutElevationRow { @@ -67,11 +93,13 @@ export interface AbutElevationRow { rotDeg: number; } -export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heightMm: number): AbutElevationRow[] { +export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): AbutElevationRow[] { const half = ABUT_BLOCK_MM / 2000; - const plan = abutSolplatePlan(rule, heightMm); + const soles = new Map(abutSoles(model, rule, heightMm, ctx).map((x) => [x.no, x])); return model.bearings.map((b) => { - const h1 = b.blockHeightMm - ABUT_PAD_MM; + const plan = soles.get(b.no)!; + // 거더 하면 고정: the block concrete loses T − T_ref (2026-11-06, Q-SH7). + const h1 = b.blockHeightMm - ABUT_PAD_MM - plan.dropMm; const E = h1 + ABUT_PAD_MM + plan.h3 + plan.T; const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half); return { @@ -81,7 +109,7 @@ export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heigh seatEl: b.seatEl, h1, h2: ABUT_PAD_MM, - z: b.z, + z: b.z - plan.dropMm / 1000, h3: plan.h3, h4: plan.T, E, @@ -98,37 +126,47 @@ export interface AbutSolplateRow { no: number; /** 기울기 (%). */ S: number; + /** S 근거 (계산 / 입력 / 가정 / 수정됨 / 없음) and the 짝 받침, e.g. "P1-①". */ + source: string; + pair?: string; L: number; W: number; T: number; T1: number; T2: number; - /** 검산 문구 "T1 − T2 = L·|S|/100". */ + /** 검산 문구 "T1 − T2 ≈ L·|S|/100" (W08, 1 mm 이내). */ check: string; + warnings: string[]; } -export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, heightMm: number): AbutSolplateRow[] { - const p = abutSolplatePlan(rule, heightMm); - return model.bearings.map((b) => ({ - no: b.no, - S: p.slopePct, - L: p.L, - W: p.W, - T: p.T, - T1: p.T1, - T2: p.T2, - check: `${(p.T1 - p.T2).toFixed(1)} = ${((p.L * Math.abs(p.slopePct)) / 100).toFixed(1)}`, - })); +export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): AbutSolplateRow[] { + return abutSoles(model, rule, heightMm, ctx).map((p) => { + const rhs = (p.L * Math.abs(p.slopePct)) / 100; + return { + no: p.no, S: p.slopePct, source: p.slopeSource, + pair: p.pair ? `${p.pair.structure}-${CIRCLED[p.pair.no - 1] ?? p.pair.no}` : undefined, + L: p.L, W: p.W, T: p.T, T1: p.T1, T2: p.T2, + check: `${p.T1 - p.T2} ≈ ${rhs.toFixed(1)} ${Math.abs(p.T1 - p.T2 - rhs) <= 1 + 1e-9 ? '일치' : '불일치 (W08)'}`, + warnings: p.warnings, + }; + }); } -/** Input problems of the 교대 받침 규칙 (the pier rule's checks with the 교대 H). Empty when it can build a solplate. */ +/** Input problems of the 교대 솔플레이트 입력. Empty when it can build a solplate. */ export function abutBearingRuleIssues(rule: AbutBearingRule, heightMm: number): string[] { - return solplateRuleIssues(abutSolplateRule(rule, heightMm)); + const out: string[] = []; + for (const k of ['tRefMm', 'tMinMm', 'aMm', 'bMm'] as const) if (!(Number.isFinite(rule[k]) && rule[k] > 0)) out.push(`${k}는 0보다 커야 합니다.`); + if (!(Number.isFinite(rule.marginMm) && rule.marginMm >= 0)) out.push('솔플 여유는 0 이상이어야 합니다.'); + if (rule.tMm !== undefined && !(Number.isFinite(rule.tMm) && rule.tMm > 0)) out.push('T는 0보다 커야 합니다.'); + if (rule.slopePct !== undefined && !Number.isFinite(rule.slopePct)) out.push('S는 숫자여야 합니다.'); + const h3 = rule.h3Mm ?? heightMm - rule.tRefMm; + if (!(Number.isFinite(h3) && h3 > 0)) out.push(`받침 높이 h3 (${h3})는 0보다 커야 합니다. 받침높이 ${heightMm}와 T_ref를 확인하세요.`); + return out; } /** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */ -export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule, heightMm: number): string[] { - const rows = abutElevationRows(model, rule, heightMm); +export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule, heightMm: number, ctx: AbutSoleContext = {}): string[] { + const rows = abutElevationRows(model, rule, heightMm, ctx); const minH1 = Math.min(...rows.map((r) => r.h1)); const half = ABUT_BLOCK_MM / 2000; // Block plan across the wall: centre t = frontSide × lateral, between the 흉벽 front (t 0) and the 벽체 front. diff --git a/src/core/abut/edit.ts b/src/core/abut/edit.ts index f8e0186..6504ee0 100644 --- a/src/core/abut/edit.ts +++ b/src/core/abut/edit.ts @@ -267,20 +267,30 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string; group: 'w ]; /** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 H는 교대 받침 시트 값으로 고정 (2026-11-03). */ -export const BEARING_RULE_FIELDS: { key: 'lengthMm' | 'widthMm' | 'slopePct'; label: string }[] = [ - { key: 'lengthMm', label: '솔플레이트 교직 L (mm)' }, - { key: 'widthMm', label: '솔플레이트 교축 W (mm)' }, - { key: 'slopePct', label: '종단 기울기 S (%)' }, +/** 교대 솔플레이트 입력 칸 (2026-11-06). Optional ones (h3, T, S) go back to the automatic value when cleared. */ +export const BEARING_RULE_FIELDS: { key: keyof AbutBearingRule; label: string; optional?: boolean }[] = [ + { key: 'h3Mm', label: '받침 높이 h3 (mm, 비우면 받침높이 − T_ref)', optional: true }, + { key: 'tRefMm', label: 'T_ref · 엑셀 z 산정 때 두께 (mm)' }, + { key: 'tMinMm', label: '솔플 최소두께 T_min (mm)' }, + { key: 'marginMm', label: '솔플 여유 L − A = W − B (mm)' }, + { key: 'aMm', label: '받침 A · 거더 방향 (mm)' }, + { key: 'bMm', label: '받침 B (mm)' }, + { key: 'tMm', label: '솔플레이트 T (mm, 비우면 자동)', optional: true }, + { key: 'slopePct', label: 'S 직접 수정 (%, 비우면 짝 받침 계산)', optional: true }, ]; /** - * Changes one 받침 규칙 value. L and W must be greater than 0, S any number, and the rule must still build a solplate with - * the 교대 받침높이 H (the pier rule's checks). + * Changes one 솔플레이트 입력 value (2026-11-06); `undefined` clears an optional one back to automatic. The result must + * still build a solplate (positive sizes, h3 > 0 with the 교대 받침높이). */ -export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number): AbutRecord { - const n = Number(value); - if (!Number.isFinite(n)) throw new Error('솔플레이트 규칙 값은 숫자여야 합니다.'); - const bearingRule = { ...(rec.bearingRule ?? {}), [key]: n }; +export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number | undefined): AbutRecord { + const bearingRule: Record = { ...(rec.bearingRule ?? {}) }; + if (value === undefined) delete bearingRule[key]; + else { + const n = Number(value); + if (!Number.isFinite(n)) throw new Error('솔플레이트 입력 값은 숫자여야 합니다.'); + bearingRule[key] = n; + } const issues = abutBearingRuleIssues(resolveBearingRule(bearingRule), rec.bearingHeightMm); if (issues.length > 0) throw new Error(issues[0]); return { ...rec, bearingRule }; diff --git a/src/core/solplate-rule.ts b/src/core/solplate-rule.ts deleted file mode 100644 index b04a310..0000000 --- a/src/core/solplate-rule.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * 솔플레이트 규칙 (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; -} diff --git a/src/main.ts b/src/main.ts index e8d4698..a0280b8 100644 --- a/src/main.ts +++ b/src/main.ts @@ -62,7 +62,6 @@ import { ABUT_PAD_PLAN_MM, abutBearingChecks, abutElevationRows, - abutSolplatePlan, abutSolplateRows, resolveBearingRule, } from './core/abut/bearing-table'; @@ -120,7 +119,6 @@ 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 { 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'; @@ -955,9 +953,10 @@ function fillAbutBearingCard( } const m3 = (v: number) => v.toFixed(3); const H = rec.bearingHeightMm; - const plan = abutSolplatePlan(rule, H); - const elev = abutElevationRows(model, rule, H); - const sol = abutSolplateRows(model, rule, H); + // 솔플레이트 S: 짝 받침(P1 전열 등) 계산, 없으면 교대 종단경사 i (2026-11-06). + const ctx = { pairs: state.bridgePairs, slopeI: rec.slopeI }; + const elev = abutElevationRows(model, rule, H, ctx); + const sol = abutSolplateRows(model, rule, H, ctx); // 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing. const pickNo = elev.some((r) => r.no === state.abutBearingNo) ? state.abutBearingNo : elev[0].no; box.append( @@ -991,7 +990,7 @@ function fillAbutBearingCard( figs.append(fig); } box.append(figs); - box.append(el('p', `받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3 = H − T, h4 = T (솔플레이트 규칙)`, 'lbl')); + box.append(el('p', `받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3 받침 (기본 받침높이 ${H} − T_ref), h4 = T · 거더 하면 고정`, 'lbl')); box.append( table( ['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'], @@ -1003,40 +1002,37 @@ function fillAbutBearingCard( ]), ), ); - box.append(el('p', '솔플레이트 상세 TABLE (mm) · 솔플레이트 규칙', 'lbl')); + box.append(el('p', '솔플레이트 상세 TABLE (mm) · S는 같은 거더 양끝 받침에서 계산', 'lbl')); box.append( table( - ['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 = L·|S|/100'], - sol.map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), r.T1.toFixed(1), r.T2.toFixed(1), r.check]), + ['No.', '짝 받침', 'S (%)', 'S 근거', 'L', 'W', 'T', 'T1 (오르막 쪽)', 'T2', '검산 T1 − T2 ≈ L·|S|/100', '검사'], + sol.map((r) => [String(r.no), r.pair ?? '없음', r.S.toFixed(3), r.source, String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check, r.warnings.length ? r.warnings.join(' · ') : '—']), ), ); - // 솔플레이트 규칙 (교각 카드와 같은 형식, 2026-11-03): H는 교대 받침 시트 값으로 고정, L · W · S를 입력한다. - const d = ABUT_BEARING_RULE_DEFAULT; - box.append(el('p', `솔플레이트 규칙 (이 교대 · 기본 ${d.lengthMm} × ${d.widthMm} · ${d.slopePct} %) · 받침높이 H ${H} mm 고정 (솔플레이트 포함)`, 'lbl')); + // 솔플레이트 입력 (결정 2026-11-06): 교각 카드와 같은 항목, 받침이 1열이라 T 하나. + box.append(el('p', `솔플레이트 입력 (이 교대) · T = max(T_min, ⌈T_min + L·|S|/2⌉) · 거더 하면 고정 → 블럭 상면 = z − (T − T_ref)`, 'lbl')); + const auto = sol[0]; for (const f of BEARING_RULE_FIELDS) { const id = idOf(`rule-${f.key}`); const lab = el('label', f.label); lab.htmlFor = id; - const input = numberInput(id, String(rule[f.key])); - if (rule[f.key] !== ABUT_BEARING_RULE_DEFAULT[f.key]) input.classList.add('changed-input'); + const stored = (rec.bearingRule as Record | undefined)?.[f.key]; + const value = f.optional ? (typeof stored === 'number' ? String(stored) : '') : String(rule[f.key]); + const input = numberInput(id, value); + const base = f.key === 'h3Mm' ? `${H - rule.tRefMm}` : f.key === 'tMm' ? `자동 ${auto?.T ?? '—'}` : f.key === 'slopePct' ? `계산 ${auto ? auto.S.toFixed(3) : '—'}` : String(ABUT_BEARING_RULE_DEFAULT[f.key]); + if (f.optional) input.placeholder = base; + if (typeof stored === 'number' && stored !== ABUT_BEARING_RULE_DEFAULT[f.key]) input.classList.add('changed-input'); input.addEventListener('change', () => { try { - apply(applyBearingRule(rec, f.key, Number(input.value))); + apply(applyBearingRule(rec, f.key, input.value.trim() === '' && f.optional ? undefined : Number(input.value))); } catch (err) { - fail(err, input, String(rule[f.key])); + fail(err, input, value); } }); - box.append(lab, input, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT[f.key]}`, 'muted small')); + box.append(lab, input, el('span', `기본 ${base}`, 'muted small')); } - box.append( - el( - 'p', - `중심 두께 T = ${plan.T} mm (T2 ≥ ${SOLPLATE_MIN_T2_MM} mm) · 받침 본체 h3 = H − T = ${plan.h3} mm · T1 = ${plan.T1.toFixed(1)} mm · T2 = ${plan.T2.toFixed(1)} mm`, - 'muted small', - ), - ); box.append(el('p', `블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small')); - box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule, H).join(' · ')}`, 'muted small abut-checks')); + box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule, H, ctx).join(' · ')}`, 'muted small abut-checks')); } /** diff --git a/tests/abut-bearing-table.test.ts b/tests/abut-bearing-table.test.ts index 547bc60..cf3026c 100644 --- a/tests/abut-bearing-table.test.ts +++ b/tests/abut-bearing-table.test.ts @@ -9,12 +9,12 @@ import { ABUT_BEARING_RULE_DEFAULT, abutBearingChecks, abutElevationRows, - abutSolplatePlan, abutSolplateRows, resolveBearingRule, } from '../src/core/abut/bearing-table'; import { applyBearingRule } from '../src/core/abut/edit'; import { checkStoredAbutments } from '../src/core/project'; +import { p1Pairs } from './helpers'; const RAG = fileURLToPath(new URL('../../../RAG/운곡교P1입력.xlsx', import.meta.url)); const rec = parseAbutments(XLSX.read(readFileSync(RAG), { type: 'buffer' })).abutments[0]; @@ -23,20 +23,23 @@ const model = buildAbut(input); const members = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm }); const rule = resolveBearingRule(); const H = rec.bearingHeightMm; +// Bridge context as in the app: A1 ①–⑤ pair with P1 전열 ①–⑤ (2026-11-06), 교대 종단경사 i from the sheet. +const ctx = { pairs: p1Pairs(), slopeI: rec.slopeI }; -describe('교대 받침 카드 (교각 솔플레이트 규칙, 받침높이 H = 교대 시트 값)', () => { - it('the rule defaults are the pier rule: 1000 × 1100 · −0.5 %, H 275 from the 교대 sheet', () => { - expect(ABUT_BEARING_RULE_DEFAULT).toMatchObject({ lengthMm: 1000, widthMm: 1100, slopePct: -0.5 }); +describe('교대 받침 카드 (솔플레이트 결정 2026-11-06, h3 = 받침높이 − T_ref)', () => { + it('defaults: A 700 × B 640 + 100, T_ref 30, T_min 22; H 275 from the 교대 sheet → h3 245', () => { + expect(ABUT_BEARING_RULE_DEFAULT).toMatchObject({ tRefMm: 30, tMinMm: 22, marginMm: 100, aMm: 700, bMm: 640 }); expect(H).toBe(275); - // T = ⌈22 + 1000·0.5/200⌉ = 25, h3 = 275 − 25 = 250, T1/T2 = 25 ± 2.5 - expect(abutSolplatePlan(rule, H)).toMatchObject({ T: 25, h3: 250, T1: 27.5, T2: 22.5 }); + const [r] = abutSolplateRows(model, rule, H, ctx); + expect([r.L, r.W, r.T, r.T1, r.T2]).toEqual([800, 740, 30, 38, 22]); + expect(abutElevationRows(model, rule, H, ctx)[0].h3).toBe(245); }); - it('ELEVATION: block 125 = 75 + 50, E = 75 + 50 + 250 + 25 = 400', () => { - const rows = abutElevationRows(model, rule, H); + it('ELEVATION: block 125 = 75 + 50, E = 75 + 50 + 245 + 30 = 400 (the girder underside of the sheet)', () => { + const rows = abutElevationRows(model, rule, H, ctx); expect(rows).toHaveLength(5); for (const r of rows) { - expect([r.h1, r.h2, r.h3, r.h4, r.E]).toEqual([75, 50, 250, 25, 400]); + expect([r.h1, r.h2, r.h3, r.h4, r.E]).toEqual([75, 50, 245, 30, 400]); expect(r.solplateTopEl).toBeCloseTo(r.seatEl + 0.4, 6); } const first = rows.find((r) => r.no === 1)!; @@ -45,21 +48,29 @@ describe('교대 받침 카드 (교각 솔플레이트 규칙, 받침높이 H = expect(first.z).toBeCloseTo(44.492, 3); }); - it('솔플레이트 TABLE: L 1000, W 1100, S −0.5 %, T 25, T1 27.5 / T2 22.5, check 5.0 = 5.0', () => { - const [r] = abutSolplateRows(model, rule, H); - expect([r.L, r.W, r.S, r.T, r.T1, r.T2]).toEqual([1000, 1100, -0.5, 25, 27.5, 22.5]); - expect(r.check).toBe('5.0 = 5.0'); + it('솔플레이트 TABLE: S from the P1 pair (−1.908 %, 계산), T1 38 on the 오르막 (A1) side; W08 within 1 mm', () => { + const rows = abutSolplateRows(model, rule, H, ctx); + expect(rows.map((r) => r.S.toFixed(3))).toEqual(['-1.908', '-1.906', '-1.904', '-1.904', '-1.902']); + expect(rows.every((r) => r.source === '계산' && r.pair?.startsWith('P1-'))).toBe(true); + expect(rows[0].check).toBe('16 ≈ 15.3 일치'); }); - it('the rule inputs change the plan: S −1.91 gives T 32 and h3 243', () => { - const steep = resolveBearingRule({ slopePct: -1.91 }); - // T = ⌈22 + 1000·1.91/200⌉ = ⌈31.55⌉ = 32 - expect(abutSolplatePlan(steep, H)).toMatchObject({ T: 32, h3: 243 }); - expect(abutElevationRows(model, steep, H)[0].E).toBe(75 + 50 + 275); + it('without a pair the 교대 종단경사 i (−0.0191) is used: S −1.91 % (입력), T 30', () => { + const [r] = abutSolplateRows(model, rule, H, { slopeI: rec.slopeI }); + expect(r.source).toBe('입력'); + expect(r.S).toBeCloseTo(-1.91, 9); + expect(r.T).toBe(30); + }); + + it('S typed −3 % → T 34; the block concrete loses 4 mm and E stays 400 (거더 하면 고정)', () => { + const steep = resolveBearingRule({ slopePct: -3 }); + expect(abutSolplateRows(model, steep, H, ctx)[0]).toMatchObject({ T: 34, T1: 46, T2: 22, source: '수정됨' }); + const r = abutElevationRows(model, steep, H, ctx)[0]; + expect([r.h1, r.h4, r.E]).toEqual([71, 34, 400]); }); it('checks: W02, W04 (흉벽 ≈ 145 · 전면 255), W03 and I01 all clear for A1', () => { - const lines = abutBearingChecks(model, members, rule, H); + const lines = abutBearingChecks(model, members, rule, H, ctx); expect(lines).toHaveLength(4); expect(lines[0]).toContain('W02 없음'); // 시안 145 is bearing ① (lateral 0.745); the smallest gap over the five bearings is 144 (lateral 0.744). @@ -70,12 +81,17 @@ describe('교대 받침 카드 (교각 솔플레이트 규칙, 받침높이 H = expect(lines[3]).toContain('I01 없음'); }); - it('the rule edit keeps only changed values, refuses L 0 and a rule thicker than H, and the project file accepts it', () => { - const edited = applyBearingRule(applyBearingRule(rec, 'lengthMm', 900), 'widthMm', 1000); - expect(edited.bearingRule).toEqual({ lengthMm: 900, widthMm: 1000 }); - expect(() => applyBearingRule(rec, 'lengthMm', 0)).toThrow('0보다'); - expect(() => applyBearingRule(rec, 'slopePct', 60)).toThrow('받침높이'); + it('the edit keeps only changed values, clears optional ones, refuses non-positive sizes and h3 ≤ 0, and the project file accepts it', () => { + const edited = applyBearingRule(applyBearingRule(rec, 'aMm', 650), 'tMm', 32); + expect(edited.bearingRule).toEqual({ aMm: 650, tMm: 32 }); + expect(applyBearingRule(edited, 'tMm', undefined).bearingRule).toEqual({ aMm: 650 }); + expect(() => applyBearingRule(rec, 'aMm', 0)).toThrow('0보다'); + expect(() => applyBearingRule(rec, 'tRefMm', 300)).toThrow('h3'); expect(() => checkStoredAbutments([edited])).not.toThrow(); - expect(() => checkStoredAbutments([{ ...edited, bearingRule: { lengthMm: 'x' } }])).toThrow('받침 규칙'); + expect(() => checkStoredAbutments([{ ...edited, bearingRule: { aMm: 'x' } }])).toThrow('받침 규칙'); + }); + + it('values stored by the 2026-11-03 C안 (lengthMm, widthMm) are ignored', () => { + expect(resolveBearingRule({ lengthMm: 900, widthMm: 1000 } as Record)).toEqual(ABUT_BEARING_RULE_DEFAULT); }); }); diff --git a/tests/solplate-rule.test.ts b/tests/solplate-rule.test.ts deleted file mode 100644 index 18ad4b9..0000000 --- a/tests/solplate-rule.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { SOLPLATE_RULE_DEFAULT, solplateCenterT, solplatePlan, solplateRuleIssues } from '../src/core/solplate-rule'; - -describe('솔플레이트 규칙 (C안, 2026-11-03)', () => { - it('default 211 · 1000 × 1100 · −0.5 %: T = 25, T1 = 27.5, T2 = 22.5, h3 = 211 − 25 = 186', () => { - const p = solplatePlan(SOLPLATE_RULE_DEFAULT); - expect(p.T).toBe(25); - expect(p.T1).toBeCloseTo(27.5, 9); - expect(p.T2).toBeCloseTo(22.5, 9); - expect(p.h3).toBe(186); - expect(p.L).toBe(1000); - expect(p.W).toBe(1100); - }); - - it('the centre thickness is the smallest whole mm that keeps T2 at 22 mm or more', () => { - for (const [L, S] of [[1000, -0.5], [1100, -0.5], [1000, -2], [800, -1.91], [1200, -3]] as const) { - const T = solplateCenterT(L, S); - expect(T - (L * Math.abs(S)) / 200).toBeGreaterThanOrEqual(22); - expect(solplateCenterT(L, S) - 1 - (L * Math.abs(S)) / 200).toBeLessThan(22); - } - }); - - it('T1 + T2 = 2T for any rule', () => { - const p = solplatePlan({ heightMm: 220, lengthMm: 900, widthMm: 800, slopePct: -1.2 }); - expect(p.T1 + p.T2).toBeCloseTo(2 * p.T, 9); - }); - - it('the default rule has no input issues', () => { - expect(solplateRuleIssues(SOLPLATE_RULE_DEFAULT)).toEqual([]); - }); - - it('reports a bearing height that does not exceed the solplate centre thickness', () => { - expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, heightMm: 25 })).toHaveLength(1); - }); - - it('reports non-positive plan sizes and a non-numeric slope', () => { - expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, lengthMm: 0 }).length).toBeGreaterThan(0); - expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, widthMm: -1 }).length).toBeGreaterThan(0); - expect(solplateRuleIssues({ ...SOLPLATE_RULE_DEFAULT, slopePct: NaN }).length).toBeGreaterThan(0); - }); -}); diff --git a/tests/solplate.test.ts b/tests/solplate.test.ts index 118f4cc..5605f00 100644 --- a/tests/solplate.test.ts +++ b/tests/solplate.test.ts @@ -1,42 +1,40 @@ import { describe, expect, it } from 'vitest'; import { buildPier } from '../src/core/geometry'; import { solplateGeometry } from '../src/core/solplate'; -import { SOLPLATE_RULE_DEFAULT, solplatePlan } from '../src/core/solplate-rule'; -import { loadP1 } from './helpers'; +import { loadP1, p1Pairs } from './helpers'; -const model = buildPier(loadP1()); +// P1 with its A1 girder pairs (2026-11-06): S −1.908 %, L 800 × W 740, T 30, T1 38 / T2 22, h3 181. +const model = buildPier(loadP1(), { pairs: p1Pairs() }); const front1 = model.blocks.filter((b) => b.row === '전열')[0]; -const plan = solplatePlan(SOLPLATE_RULE_DEFAULT); -describe('솔플레이트 기하 (C안, 2026-11-03)', () => { - it('L = 교직 1000 across the 교각축 직각 (n) and W = 교축 1100 along the 교축 (s)', () => { - const g = solplateGeometry(front1, plan); - expect(g.L).toBeCloseTo(1.0, 6); - expect(g.W).toBeCloseTo(1.1, 6); +describe('솔플레이트 기하 (결정 2026-11-06)', () => { + it('L = A + 100 = 800 along the girder and W = B + 100 = 740 across it', () => { + const g = solplateGeometry(front1, front1.plan); + expect(g.L).toBeCloseTo(0.8, 6); + expect(g.W).toBeCloseTo(0.74, 6); }); - it('the thick end T1 is on the (−) side: T1 = 27.5 mm, T2 = 22.5 mm, and T1 + T2 = 2T', () => { - const g = solplateGeometry(front1, plan); - expect(g.T1 * 1000).toBeCloseTo(27.5, 6); - expect(g.T2 * 1000).toBeCloseTo(22.5, 6); - expect((g.T1 + g.T2) * 1000).toBeCloseTo(50, 6); - // The top at the (−) corners is the thick side. - const topAtStart = g.corners.filter((c) => Math.abs(c[1] - (g.bottom + g.T1)) < 1e-9); - expect(topAtStart.length).toBe(2); + it('T1 = 38 mm on one end, T2 = 22 mm on the other, and T1 + T2 = 2T', () => { + const g = solplateGeometry(front1, front1.plan); + expect(g.T1 * 1000).toBeCloseTo(38, 6); + expect(g.T2 * 1000).toBeCloseTo(22, 6); + expect((g.T1 + g.T2) * 1000).toBeCloseTo(60, 6); + const thick = g.corners.filter((c) => Math.abs(c[1] - (g.bottom + g.T1)) < 1e-9); + expect(thick.length).toBe(2); }); - it('the underside is the bearing top: block top (EL.X) + h3 (186 mm)', () => { - const g = solplateGeometry(front1, plan); - expect(g.bottom).toBeCloseTo(front1.padTop + 0.186, 6); + it('the underside is the bearing top: pad top (EL.X) + h3 (181 mm)', () => { + const g = solplateGeometry(front1, front1.plan); + expect(g.bottom).toBeCloseTo(front1.padTop + 0.181, 6); }); - it('volume = L·W·T (the taper averages to the centre thickness)', () => { - const g = solplateGeometry(front1, plan); - expect(g.volume).toBeCloseTo(g.L * g.W * 0.025, 9); + it('volume = L·W·T (the taper averages to the row thickness)', () => { + const g = solplateGeometry(front1, front1.plan); + expect(g.volume).toBeCloseTo(g.L * g.W * 0.03, 9); }); it('the solplate is centred on the block and follows the row direction', () => { - const g = solplateGeometry(front1, plan); + const g = solplateGeometry(front1, front1.plan); const c = g.corners.reduce((acc, p) => [acc[0] + p[0] / 8, acc[1] + p[2] / 8] as [number, number], [0, 0]); expect(c[0]).toBeCloseTo(front1.s, 6); expect(c[1]).toBeCloseTo(front1.n, 6);