diff --git a/src/core/abut/bearing-table.ts b/src/core/abut/bearing-table.ts index b4ad9b5..43a5066 100644 --- a/src/core/abut/bearing-table.ts +++ b/src/core/abut/bearing-table.ts @@ -1,29 +1,29 @@ /** * 교대 받침 카드 (2026-11-03, 시안 ② 교량받침·받침블럭·솔플레이트, 교각 카드와 같은 형식). 순수 함수. * - * - 교대 받침 상세도가 없어 받침 A × B, h3, 솔플레이트는 교각 P1 전열 값을 가정한다 (spec_abut QA-15). - * - 높이: E = h1 (블럭 콘크리트) + h2 (몰탈패드 50) + h3 (받침) + h4 (솔플레이트 T). z = 패드 상면 = EL.X (결정 2026-11-03). - * - 솔플레이트 경사 S = 종단경사 i. 두께 T1·T2 = T ± L·|S| / 2 (반올림), 검산 T1 − T2 ≈ L·|S|. + * - 솔플레이트는 교각과 같은 규칙(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. */ +import { solplatePlan, solplateRuleIssues } from '../solplate-rule'; +import type { SolplatePlan, SolplateRule } from '../types'; import { ABUT_PAD_MM, type AbutModel } from './geometry'; import type { AbutMembers } from './members'; export interface AbutBearingRule { - /** 받침 높이 h3 (mm). */ - h3Mm: number; - /** 솔플레이트 두께 T (mm) = h4. */ - solplateTMm: number; - /** 솔플레이트 여유 L − A, W − B (mm). */ - solplateMarginMm: number; + /** 솔플레이트 교직 L (mm). */ + lengthMm: number; + /** 솔플레이트 교축 W (mm). */ + widthMm: number; + /** 종단 기울기 S (%). */ + slopePct: number; /** 받침 종류 · 용량 (글자 입력). */ kind: string; } -/** 기본값: 교각 P1 전열 값 (가정, QA-15). 받침 종류는 탄성받침 (Task yaml 응답). */ -export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { h3Mm: 181, solplateTMm: 30, solplateMarginMm: 100, kind: '탄성받침' }; -/** 받침 A × B (mm), 교각 P1 전열 가정 (QA-15). */ -export const ABUT_BEARING_A_MM = 700; -export const ABUT_BEARING_B_MM = 640; +/** 기본값: 교각 솔플레이트 규칙과 같은 1000 × 1100 · −0.5 % (가정, QA-15). 받침 종류는 탄성받침 (Task yaml 응답). */ +export const ABUT_BEARING_RULE_DEFAULT: AbutBearingRule = { lengthMm: 1000, widthMm: 1100, slopePct: -0.5, kind: '탄성받침' }; /** 받침블럭 평면 (mm, 정사각) and 몰탈패드 평면 (mm), spec_abut §4.2. */ export const ABUT_BLOCK_MM = 1200; export const ABUT_PAD_PLAN_MM = 1000; @@ -36,6 +36,15 @@ export function resolveBearingRule(changes?: Partial): AbutBear return { ...ABUT_BEARING_RULE_DEFAULT, ...(changes ?? {}) }; } +/** 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 }; +} + +export function abutSolplatePlan(rule: AbutBearingRule, heightMm: number): SolplatePlan { + return solplatePlan(abutSolplateRule(rule, heightMm)); +} + export interface AbutElevationRow { no: number; /** IP3(① 쪽)에서 잰 위치 (m). */ @@ -51,8 +60,6 @@ export interface AbutElevationRow { E: number; /** 솔플레이트 상면 EL (m) = 교좌면 + E. */ solplateTopEl: number; - A: number; - B: number; /** 블럭 끝에서 가장 가까운 계단 경계까지 (m). 경계가 없으면 null. */ stepGapM: number | null; /** 받침 방위각 (deg): CAD (엑셀 값, 동 = 0° 반시계) / 진북 (북에서 시계). 입력이 없으면 null. */ @@ -62,11 +69,12 @@ export interface AbutElevationRow { rotDeg: number; } -export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): AbutElevationRow[] { +export function abutElevationRows(model: AbutModel, rule: AbutBearingRule, heightMm: number): AbutElevationRow[] { const half = ABUT_BLOCK_MM / 2000; + const plan = abutSolplatePlan(rule, heightMm); return model.bearings.map((b) => { const h1 = b.blockHeightMm - ABUT_PAD_MM; - const E = h1 + ABUT_PAD_MM + rule.h3Mm + rule.solplateTMm; + const E = h1 + ABUT_PAD_MM + plan.h3 + plan.T; const gaps = model.stepBounds.map((x) => Math.abs(x - b.s) - half); return { no: b.no, @@ -76,12 +84,10 @@ export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): Abut h1, h2: ABUT_PAD_MM, z: b.z, - h3: rule.h3Mm, - h4: rule.solplateTMm, + h3: plan.h3, + h4: plan.T, E, solplateTopEl: b.seatEl + E / 1000, - A: ABUT_BEARING_A_MM, - B: ABUT_BEARING_B_MM, stepGapM: gaps.length ? Math.min(...gaps) : null, azimuthCadDeg: b.azimuthDeg === undefined ? null : (((90 - b.azimuthDeg) % 360) + 360) % 360, azimuthNorthDeg: b.azimuthDeg === undefined ? null : ((b.azimuthDeg % 360) + 360) % 360, @@ -92,38 +98,39 @@ export function abutElevationRows(model: AbutModel, rule: AbutBearingRule): Abut export interface AbutSolplateRow { no: number; - /** 경사 (%) = 종단경사 i. */ + /** 기울기 (%). */ S: number; L: number; W: number; T: number; T1: number; T2: number; - /** 검산 문구 "T1 − T2 ≈ L·|S|". */ + /** 검산 문구 "T1 − T2 = L·|S|/100". */ check: string; } -export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, slopeI: number): AbutSolplateRow[] { - const L = ABUT_BEARING_A_MM + rule.solplateMarginMm; - const W = ABUT_BEARING_B_MM + rule.solplateMarginMm; - const rise = L * Math.abs(slopeI); - const T1 = Math.round(rule.solplateTMm + rise / 2); - const T2 = Math.round(rule.solplateTMm - rise / 2); +export function abutSolplateRows(model: AbutModel, rule: AbutBearingRule, heightMm: number): AbutSolplateRow[] { + const p = abutSolplatePlan(rule, heightMm); return model.bearings.map((b) => ({ no: b.no, - S: slopeI * 100, - L, - W, - T: rule.solplateTMm, - T1, - T2, - check: `${T1 - T2} ≈ ${rise.toFixed(1)}`, + 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)}`, })); } +/** Input problems of the 교대 받침 규칙 (the pier rule's checks with the 교대 H). Empty when it can build a solplate. */ +export function abutBearingRuleIssues(rule: AbutBearingRule, heightMm: number): string[] { + return solplateRuleIssues(abutSolplateRule(rule, heightMm)); +} + /** 검사 줄 (시안): W02 블럭 콘크리트, W04 블럭이 교좌면 안, W03 블럭 끝 ~ 계단 경계, I01 블럭 철근. 문제 없으면 "없음". */ -export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule): string[] { - const rows = abutElevationRows(model, rule); +export function abutBearingChecks(model: AbutModel, members: AbutMembers, rule: AbutBearingRule, heightMm: number): string[] { + const rows = abutElevationRows(model, rule, heightMm); 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 5e1f15a..25b341c 100644 --- a/src/core/abut/edit.ts +++ b/src/core/abut/edit.ts @@ -5,7 +5,7 @@ import type { AbutRecord } from './parse'; import type { WingForm } from './geometry'; import { shapeIssues, type AbutShape } from './shape'; -import type { AbutBearingRule } from './bearing-table'; +import { abutBearingRuleIssues, resolveBearingRule, type AbutBearingRule } from './bearing-table'; export type AbutEditKey = 'seatEl' | 'foundationBottomEl' | 'groundEl' | 'pileLengthMm' | 'skewDeg' | 'slopeI'; @@ -87,14 +87,17 @@ export const SHAPE_EDIT_FIELDS: { key: keyof AbutShape; label: string }[] = [ { key: 'parapetLevelRightMm', label: '흉벽 상단 Level 우 (mm, 계산 미사용)' }, ]; -/** 받침 규칙 입력칸 (② 교량받침 카드, 시안 공통 · 규칙). 기본값은 교각 P1 전열 값 (가정, QA-15). */ -export const BEARING_RULE_FIELDS: { key: 'h3Mm' | 'solplateTMm' | 'solplateMarginMm'; label: string }[] = [ - { key: 'h3Mm', label: '받침 높이 h3 (mm)' }, - { key: 'solplateTMm', label: '솔플레이트 T (mm)' }, - { key: 'solplateMarginMm', label: '솔플 여유 L − A, W − B (mm)' }, +/** 받침 규칙 입력칸 (② 교량받침 카드): 교각 솔플레이트 규칙과 같은 칸. 받침높이 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 (%)' }, ]; -/** Changes one 받침 규칙 value: the numbers must be greater than 0, the 받침 종류 must not be empty. */ +/** + * 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). The 받침 종류 must not be empty. + */ export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, value: number | string): AbutRecord { if (key === 'kind') { const kind = String(value).trim(); @@ -102,7 +105,9 @@ export function applyBearingRule(rec: AbutRecord, key: keyof AbutBearingRule, va return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), kind } }; } const n = Number(value); - if (!(Number.isFinite(n) && n > 0)) throw new Error('받침 규칙 값은 0보다 큰 숫자여야 합니다.'); - return { ...rec, bearingRule: { ...(rec.bearingRule ?? {}), [key]: n } }; + if (!Number.isFinite(n)) throw new Error('솔플레이트 규칙 값은 숫자여야 합니다.'); + const bearingRule = { ...(rec.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/main.ts b/src/main.ts index d00497d..f15793b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -18,6 +18,7 @@ import { ABUT_PAD_PLAN_MM, abutBearingChecks, abutElevationRows, + abutSolplatePlan, abutSolplateRows, resolveBearingRule, } from './core/abut/bearing-table'; @@ -68,7 +69,7 @@ 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_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule'; +import { SOLPLATE_MIN_T2_MM, SOLPLATE_RULE_DEFAULT, solplatePlan, solplateRuleIssues } from './core/solplate-rule'; import type { SolplateRule } from './core/types'; import { type BarSummary, elevationRows, solplateRows } from './core/bearing-table'; @@ -805,8 +806,10 @@ function fillAbutBearingCard( return; } const m3 = (v: number) => v.toFixed(3); - const elev = abutElevationRows(model, rule); - const sol = abutSolplateRows(model, rule, rec.slopeI); + const H = rec.bearingHeightMm; + const plan = abutSolplatePlan(rule, H); + const elev = abutElevationRows(model, rule, H); + const sol = abutSolplateRows(model, rule, H); // 상세 보기 (받침 ①–⑤): the three 삽도 draw the picked bearing. const pickNo = elev.some((r) => r.no === state.abutBearingNo) ? state.abutBearingNo : elev[0].no; const picker = el('div', undefined, 'abut-bearing-pick'); @@ -825,14 +828,14 @@ function fillAbutBearingCard( const er = elev.find((r) => r.no === pickNo)!; const sr = sol.find((r) => r.no === pickNo)!; const detail: AbutBearingDetail = { - no: er.no, blockMm: ABUT_BLOCK_MM, padMm: ABUT_PAD_PLAN_MM, A: er.A, B: er.B, L: sr.L, W: sr.W, + no: er.no, blockMm: ABUT_BLOCK_MM, padMm: ABUT_PAD_PLAN_MM, L: sr.L, W: sr.W, h1: er.h1, h2: er.h2, h3: er.h3, h4: er.h4, T1: sr.T1, T2: sr.T2, rotDeg: er.rotDeg, S: sr.S, seatEl: er.seatEl, z: er.z, }; const figs = el('div', undefined, 'abut-bearing-figs'); for (const [svg, caption] of [ - [abutBearingPlanSvg(detail), '평면 · 점선 = 몰탈패드, 주황 점선 = 솔플레이트'], + [abutBearingPlanSvg(detail), '평면 · 점선 = 몰탈패드, 주황 = 솔플레이트 L × W'], [abutBearingSectionSvg(detail), '단면 · 교축 방향 · E = h1 + h2 + h3 + h4'], - [abutSolplateSvg(detail), '솔플레이트 상세 · 가정'], + [abutSolplateSvg(detail), '솔플레이트 상세 · T = ⌈22 + L·|S|/200⌉'], ]) { const fig = el('figure', undefined, 'abut-fig'); fig.innerHTML = svg; @@ -840,26 +843,28 @@ function fillAbutBearingCard( figs.append(fig); } box.append(figs); - box.append(el('p', '받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3·h4는 가정 (QA-15)', 'lbl')); + box.append(el('p', `받침 ELEVATION TABLE (mm, EL·위치는 m) · 거리는 IP3(① 쪽)에서 · h3 = H − T, h4 = T (솔플레이트 규칙)`, 'lbl')); box.append( table( - ['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', 'A', 'B', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'], + ['No.', '위치 s', '구간', '교좌면 EL', 'h1 블럭', 'h2 패드', 'z = EL.X', 'h3 받침', 'h4 솔플', 'E', '솔플 상면 EL', '블럭 ~ 계단 경계', '방위각 CAD / 진북 (°)', '블럭 회전 (°)'], elev.map((r) => [ String(r.no), m3(r.sFromIp3M), r.seg, m3(r.seatEl), String(r.h1), String(r.h2), m3(r.z), String(r.h3), String(r.h4), - String(r.E), m3(r.solplateTopEl), String(r.A), String(r.B), r.stepGapM === null ? '—' : m3(r.stepGapM), + String(r.E), m3(r.solplateTopEl), r.stepGapM === null ? '—' : m3(r.stepGapM), r.azimuthCadDeg === null || r.azimuthNorthDeg === null ? '입력 없음' : `${r.azimuthCadDeg.toFixed(3)} / ${r.azimuthNorthDeg.toFixed(3)}`, r.rotDeg.toFixed(2), ]), ), ); - box.append(el('p', '솔플레이트 상세 TABLE (mm) · 가정 (교각 P1 값)', 'lbl')); + box.append(el('p', '솔플레이트 상세 TABLE (mm) · 솔플레이트 규칙', 'lbl')); box.append( table( - ['No.', 'S (%)', 'L', 'W', 'T', 'T1', 'T2', '검산 T1 − T2 ≈ L·|S|'], - sol.map((r) => [String(r.no), r.S.toFixed(3), String(r.L), String(r.W), String(r.T), String(r.T1), String(r.T2), r.check]), + ['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]), ), ); - box.append(el('p', `공통 · 규칙 — 블럭 평면 ${ABUT_BLOCK_MM} × ${ABUT_BLOCK_MM} (기본값) · 몰탈패드 ${ABUT_PAD_PLAN_MM} × ${ABUT_PAD_PLAN_MM} × 50 (기본값) · 블럭 높이 = min(z) − 교좌면 (계산)`, 'muted small')); + // 솔플레이트 규칙 (교각 카드와 같은 형식, 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')); for (const f of BEARING_RULE_FIELDS) { const id = idOf(`rule-${f.key}`); const lab = el('label', f.label); @@ -873,8 +878,16 @@ function fillAbutBearingCard( fail(err, input, String(rule[f.key])); } }); - box.append(lab, input, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT[f.key]} · 가정`, 'muted small')); + box.append(lab, input, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT[f.key]}`, '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')); const kindId = idOf('rule-kind'); const kindLab = el('label', '받침 종류 · 용량'); kindLab.htmlFor = kindId; @@ -890,7 +903,7 @@ function fillAbutBearingCard( } }); box.append(kindLab, kind, el('span', `기본 ${ABUT_BEARING_RULE_DEFAULT.kind}`, 'muted small')); - box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule).join(' · ')}`, 'muted small abut-checks')); + box.append(el('p', `검사 · ${abutBearingChecks(model, members, rule, H).join(' · ')}`, 'muted small abut-checks')); } /** Shows only the card of the open row in the ② 교대 editor; the basic items stay visible. */ diff --git a/src/view/abut-bearing-svg.ts b/src/view/abut-bearing-svg.ts index 0051e9f..81f8a9a 100644 --- a/src/view/abut-bearing-svg.ts +++ b/src/view/abut-bearing-svg.ts @@ -8,9 +8,7 @@ export interface AbutBearingDetail { /** 블럭 평면 (정사각), 몰탈패드 평면 (정사각), mm. */ blockMm: number; padMm: number; - /** 받침 A × B, 솔플레이트 L × W (mm). */ - A: number; - B: number; + /** 솔플레이트 교직 L × 교축 W (mm). 받침 본체 평면은 입력이 없어 그리지 않는다. */ L: number; W: number; /** 높이 (mm): 블럭 콘크리트 h1, 패드 h2, 받침 h3, 솔플레이트 h4 = T. */ @@ -36,23 +34,22 @@ const text = (x: number, y: number, s: string, anchor = 'middle', fill = INK) => const square = (cx: number, cy: number, w: number, h: number, style: string) => ``; -/** 평면: 블럭 1200, 몰탈패드 1000 (점선), 솔플레이트 L × W (주황 점선), 받침 A × B. 블럭은 받침 방위각만큼 돈다. */ +/** 평면: 블럭 1200, 몰탈패드 1000 (점선), 솔플레이트 L × W (주황). 블럭은 받침 방위각만큼 돈다. */ export function abutBearingPlanSvg(d: AbutBearingDetail): string { const k = 0.17; // px per mm const cx = 150; const cy = 130; const turn = `transform="rotate(${(-d.rotDeg).toFixed(3)} ${cx} ${cy})"`; return [ - ``, + ``, ``, square(cx, cy, d.blockMm * k, d.blockMm * k, `fill="#dfe5ec" stroke="${MUTED}"`), square(cx, cy, d.padMm * k, d.padMm * k, `fill="none" stroke="${MUTED}" stroke-dasharray="4 3"`), - square(cx, cy, d.L * k, d.W * k, `fill="none" stroke="${ORANGE}" stroke-dasharray="5 3"`), - square(cx, cy, d.A * k, d.B * k, `fill="#b9c3ce" stroke="${INK}"`), + square(cx, cy, d.L * k, d.W * k, `fill="#f0c9a0" fill-opacity="0.6" stroke="${ORANGE}"`), ``, text(cx, cy - (d.blockMm * k) / 2 - 8, `${d.blockMm}`), - text(cx, cy + 4, `${d.A} × ${d.B}`), - text(cx, cy + (d.blockMm * k) / 2 + 16, `솔플 ${d.L} × ${d.W} · 패드 ${d.padMm}`, 'middle', MUTED), + text(cx, cy + 4, `솔플 ${d.L} × ${d.W}`), + text(cx, cy + (d.blockMm * k) / 2 + 16, `패드 ${d.padMm} (점선)`, 'middle', MUTED), text(cx, 262, `받침 ${d.no} · 회전 ${d.rotDeg.toFixed(2)}° (방위각)`, 'middle', MUTED), '', ].join(''); @@ -74,7 +71,7 @@ export function abutBearingSectionSvg(d: AbutBearingDetail): string { ``, layer(0, d.h1, d.blockMm, `fill="#dfe5ec" stroke="${MUTED}"`), layer(d.h1, d.h2, d.padMm, `fill="#c9d1da" stroke="${MUTED}"`), - layer(d.h1 + d.h2, d.h3, d.A, `fill="#b9c3ce" stroke="${INK}"`), + layer(d.h1 + d.h2, d.h3, d.L - 100, `fill="#b9c3ce" stroke="${INK}"`), layer(d.h1 + d.h2 + d.h3, d.h4, d.L, `fill="#f0c9a0" stroke="${ORANGE}"`), text(right, at(d.h1 / 2) + 4, `h1 ${d.h1}`, 'start'), text(right, at(d.h1 + d.h2 / 2) + 4, `h2 ${d.h2}`, 'start'), diff --git a/tests/abut-bearing-azimuth.test.ts b/tests/abut-bearing-azimuth.test.ts index 55d8f6b..b990fce 100644 --- a/tests/abut-bearing-azimuth.test.ts +++ b/tests/abut-bearing-azimuth.test.ts @@ -49,7 +49,7 @@ describe('교대 받침 방위각 (엑셀 받침 시트 6번째 열)', () => { }); it('the ELEVATION table shows the 방위각 (CAD = the workbook value, 진북) and the block turn', () => { - const [r] = abutElevationRows(model, resolveBearingRule()).filter((x) => x.no === 1); + const [r] = abutElevationRows(model, resolveBearingRule(), rec.bearingHeightMm).filter((x) => x.no === 1); expect(r.azimuthCadDeg).toBeCloseTo(75 + 56 / 60 + 55.9 / 3600, 6); expect(r.azimuthNorthDeg).toBeCloseTo(90 - (75 + 56 / 60 + 55.9 / 3600), 6); expect(r.rotDeg).toBeCloseTo(model.bearings.find((b) => b.no === 1)!.rotDeg, 9); diff --git a/tests/abut-bearing-svg.test.ts b/tests/abut-bearing-svg.test.ts index 4f70318..f897a51 100644 --- a/tests/abut-bearing-svg.test.ts +++ b/tests/abut-bearing-svg.test.ts @@ -1,29 +1,29 @@ import { describe, expect, it } from 'vitest'; import { abutBearingPlanSvg, abutBearingSectionSvg, abutSolplateSvg, type AbutBearingDetail } from '../src/view/abut-bearing-svg'; -// 운곡교 A1 받침 ① (교대 받침 표의 값: 블럭 125 = 75 + 50, h3 181, T 30, L 800, T1 38 / T2 22, 회전 −4.42°). +// 운곡교 A1 받침 ① (교대 받침 표: 블럭 125 = 75 + 50, H 275 → T 25, h3 250, L 1000 × W 1100, T1 27.5 / T2 22.5, 회전 −4.42°). const d: AbutBearingDetail = { - no: 1, blockMm: 1200, padMm: 1000, A: 700, B: 640, L: 800, W: 740, - h1: 75, h2: 50, h3: 181, h4: 30, T1: 38, T2: 22, rotDeg: -4.42, S: -1.91, seatEl: 44.367, z: 44.492, + no: 1, blockMm: 1200, padMm: 1000, L: 1000, W: 1100, + h1: 75, h2: 50, h3: 250, h4: 25, T1: 27.5, T2: 22.5, rotDeg: -4.42, S: -0.5, seatEl: 44.367, z: 44.492, }; describe('교대 받침 삽도 (시안 ② 교량받침 카드)', () => { - it('평면: block, pad, solplate and bearing sizes, turned by the bearing azimuth', () => { + it('평면: block, pad and solplate L × W, turned by the bearing azimuth', () => { const svg = abutBearingPlanSvg(d); - expect(svg).toContain('700 × 640'); - expect(svg).toContain('솔플 800 × 740 · 패드 1000'); + expect(svg).toContain('솔플 1000 × 1100'); + expect(svg).toContain('패드 1000 (점선)'); expect(svg).toContain('rotate(4.420 150 130)'); - expect(svg.match(/ { + it('단면: the four layers and E = h1 + h2 + h3 + h4 = 400', () => { const svg = abutBearingSectionSvg(d); - for (const s of ['h1 75', 'h2 50', 'h3 181', 'h4 30', 'E = 336', '교좌면 44.367', 'z 44.492']) expect(svg).toContain(s); + for (const s of ['h1 75', 'h2 50', 'h3 250', 'h4 25', 'E = 400', '교좌면 44.367', 'z 44.492']) expect(svg).toContain(s); expect(svg.match(/ { const svg = abutSolplateSvg(d); - for (const s of ['T1 38', 'T2 22', 'L 800 · W 740', 'S -1.910 %']) expect(svg).toContain(s); + for (const s of ['T1 27.5', 'T2 22.5', 'L 1000 · W 1100', 'S -0.500 %']) expect(svg).toContain(s); }); }); diff --git a/tests/abut-bearing-table.test.ts b/tests/abut-bearing-table.test.ts index d41294e..1aa044d 100644 --- a/tests/abut-bearing-table.test.ts +++ b/tests/abut-bearing-table.test.ts @@ -9,6 +9,7 @@ import { ABUT_BEARING_RULE_DEFAULT, abutBearingChecks, abutElevationRows, + abutSolplatePlan, abutSolplateRows, resolveBearingRule, } from '../src/core/abut/bearing-table'; @@ -21,16 +22,22 @@ const input = toAbutInput(rec); const model = buildAbut(input); const members = buildMembers(model, { bottomEl: input.foundationBottomEl, pileLengthMm: input.pileLengthMm }); const rule = resolveBearingRule(); +const H = rec.bearingHeightMm; -describe('교대 받침 카드 표 (시안 ②, QA-15 가정)', () => { - it('ELEVATION: block 125 = concrete 75 + pad 50, E = 75 + 50 + 181 + 30 = 336', () => { - const rows = abutElevationRows(model, rule); +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 }); + 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 }); + }); + + it('ELEVATION: block 125 = 75 + 50, E = 75 + 50 + 250 + 25 = 400', () => { + const rows = abutElevationRows(model, rule, H); expect(rows).toHaveLength(5); for (const r of rows) { - expect(r.h1).toBe(75); - expect(r.h2).toBe(50); - expect(r.E).toBe(336); - expect(r.solplateTopEl).toBeCloseTo(r.seatEl + 0.336, 6); + expect([r.h1, r.h2, r.h3, r.h4, r.E]).toEqual([75, 50, 250, 25, 400]); + expect(r.solplateTopEl).toBeCloseTo(r.seatEl + 0.4, 6); } const first = rows.find((r) => r.no === 1)!; expect(first.seg).toBe('①'); @@ -38,24 +45,21 @@ describe('교대 받침 카드 표 (시안 ②, QA-15 가정)', () => { expect(first.z).toBeCloseTo(44.492, 3); }); - it('솔플레이트: L 800, W 740, S = −1.91 %, T1 38 / T2 22, check 16 ≈ 15.3', () => { - const [r] = abutSolplateRows(model, rule, rec.slopeI); - expect(r.L).toBe(800); - expect(r.W).toBe(740); - expect(r.S).toBeCloseTo(-1.91, 2); - expect([r.T, r.T1, r.T2]).toEqual([30, 38, 22]); - expect(r.check).toBe('16 ≈ 15.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('the rule inputs change the table: h3 200 gives E 355, margin 150 gives L 850', () => { - const r2 = resolveBearingRule({ h3Mm: 200, solplateMarginMm: 150 }); - expect(abutElevationRows(model, r2)[0].E).toBe(355); - expect(abutSolplateRows(model, r2, rec.slopeI)[0].L).toBe(850); - expect(ABUT_BEARING_RULE_DEFAULT.kind).toBe('탄성받침'); + 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('checks: W02, W04 (흉벽 ≈ 145 · 전면 255), W03 and I01 all clear for A1', () => { - const lines = abutBearingChecks(model, members, rule); + const lines = abutBearingChecks(model, members, rule, H); 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). @@ -66,12 +70,13 @@ describe('교대 받침 카드 표 (시안 ②, QA-15 가정)', () => { expect(lines[3]).toContain('I01 없음'); }); - it('the rule edit keeps only changed values, refuses 0 and an empty 받침 종류, and the project file accepts it', () => { - const edited = applyBearingRule(applyBearingRule(rec, 'h3Mm', 200), 'kind', '포트받침 3000 kN'); - expect(edited.bearingRule).toEqual({ h3Mm: 200, kind: '포트받침 3000 kN' }); - expect(() => applyBearingRule(rec, 'solplateTMm', 0)).toThrow('0보다 큰'); + 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), 'kind', '포트받침 3000 kN'); + expect(edited.bearingRule).toEqual({ lengthMm: 900, kind: '포트받침 3000 kN' }); + expect(() => applyBearingRule(rec, 'lengthMm', 0)).toThrow('0보다'); + expect(() => applyBearingRule(rec, 'slopePct', 60)).toThrow('받침높이'); expect(() => applyBearingRule(rec, 'kind', ' ')).toThrow('받침 종류'); expect(() => checkStoredAbutments([edited])).not.toThrow(); - expect(() => checkStoredAbutments([{ ...edited, bearingRule: { h3Mm: 'x' } }])).toThrow('받침 규칙'); + expect(() => checkStoredAbutments([{ ...edited, bearingRule: { lengthMm: 'x' } }])).toThrow('받침 규칙'); }); });