diff --git a/src/main.ts b/src/main.ts index 98b5ded..46b1f6d 100644 --- a/src/main.ts +++ b/src/main.ts @@ -31,9 +31,12 @@ import { applyParapetRebar, applyWingRebar, applySlabRebar, + applyBlockRebar, } from './core/abut/edit'; import { ABUT_FOOTING_REBAR_DEFAULT, abutFootingGeom, abutFootingRebar, ABUT_FOOTING_SIZES, type AbutFootingGeom, type AbutFootingRebar, type AbutFootingRebarSettings } from './core/abut/rebar/footing'; import { lenText, skewAngle } from './core/abut/rebar/skew-layout'; +import { ABUT_BLOCK_PAD_MM, ABUT_BLOCK_REBAR_DEFAULT, abutBlockRebar, type AbutBlockRebar, type AbutBlockSettings } from './core/abut/rebar/block'; +import { ABUT_BLOCK_COLORS, abutBlockPlanSvg as abutBlockRebarPlanSvg, abutBlockSectionSvg as abutBlockRebarSectionSvg } from './view/abut-block-rebar-svg'; import { ABUT_SLAB_REBAR_DEFAULT, ABUT_SLAB_SIZES, abutSlabGeom, abutSlabRebar, type AbutSlabGeom, type AbutSlabRebar, type AbutSlabRebarSettings } from './core/abut/rebar/slab'; import { ABUT_SLAB_COLORS, abutSlabPlanSvg as abutSlabRebarPlanSvg, abutSlabSectionSvg as abutSlabRebarSectionSvg } from './view/abut-slab-rebar-svg'; import { ABUT_WING_REBAR_DEFAULT, ABUT_WING_SIZES, abutWingGeom, abutWingRebar, type AbutWingGeom, type AbutWingRebar, type AbutWingRebarSettings } from './core/abut/rebar/wing'; @@ -234,6 +237,8 @@ const state = { abutWingView: 'front' as 'front' | 'back' | 'corner', /** ③ 교대 접속슬래브 보기 탭 (평면 / 단면). */ abutSlabView: 'plan' as 'plan' | 'sec', + /** ③ 교대 받침블럭 보기 탭 (평면 / 단면). */ + abutBlockView: 'plan' as 'plan' | 'sec', abutRebarNotes: false, /** Member card selected on ② (시안 ② 부재 목록). */ member: 'coping' as MemberId, @@ -3107,10 +3112,10 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void { box.appendChild(wrap); } -/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) · 날개벽 (AD7) · 접속슬래브 (AD8) have rules; 받침블럭 (AD6) comes next. */ +/** ③ 교대 부재 (시안 순서, plan_abut AD9). All six 부재 have rules now (AD3–AD8). */ type AbutRebarMemberId = 'blk' | 'wall' | 'para' | 'foot' | 'wing' | 'slab'; const ABUT_REBAR_MEMBERS: { id: AbutRebarMemberId; label: string; marks: string; ready: boolean }[] = [ - { id: 'blk', label: '받침블럭', marks: 'S1·S2', ready: false }, + { id: 'blk', label: '받침블럭', marks: 'S1·S2', ready: true }, { id: 'wall', label: '벽체', marks: 'W1–W11', ready: true }, { id: 'para', label: '흉벽·브라켓', marks: 'P1–P11', ready: true }, { id: 'foot', label: '확대기초', marks: 'B1–B10', ready: true }, @@ -3180,6 +3185,18 @@ function abutWingsBuilt(rec: AbutRecord, footing: AbutFootingRebar): { geom: Abu } } +/** The 받침블럭 rebar of an abutment: one block per bearing (블럭 콘크리트 = 블럭 높이 − 패드 50). */ +function abutBlockBuilt(rec: AbutRecord): { result: AbutBlockRebar } | null { + try { + const model = buildAbut(toAbutInput(rec)); + const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape); + const blocks = model.bearings.map((b) => ({ no: b.no, s: b.s, t: model.frontSide * b.lateral, rotDeg: b.rotDeg, seatEl: b.seatEl, blockHeightMm: b.blockHeightMm })); + return { result: abutBlockRebar(blocks, members.footingBottomEl ?? 0, rec.rebar?.block, resolveWallRebar(rec.rebar?.wall).c, rec.name) }; + } catch { + return null; + } +} + /** The 접속슬래브 rebar of an abutment, on the ② slab plan and section (null when it does not build). */ function abutSlabBuilt(rec: AbutRecord): { geom: AbutSlabGeom; result: AbutSlabRebar } | null { try { @@ -3211,8 +3228,11 @@ function renderAbutRebar(rec: AbutRecord): void { const para = wall ? abutParapetBuilt(rec, wall) : null; const wings = built ? abutWingsBuilt(rec, built.result) : []; const slabR = abutSlabBuilt(rec); + const blockR = abutBlockBuilt(rec); const issuesOf = (id: AbutRebarMemberId) => - (id === 'foot' + (id === 'blk' + ? blockR?.result.issues + : id === 'foot' ? built?.result.issues : id === 'wall' ? wall?.result.issues @@ -3251,7 +3271,7 @@ function renderAbutRebar(rec: AbutRecord): void { if (!member.ready) { $('rebar-note').textContent = `${rec.name} · ${member.label} · Type-1 · ${member.marks} · 다음 단계`; box.append(el('p', `${member.label} 배근은 다음 단계에서 추가합니다 (철근 기획서 확정, plan_abut AD4–AD8).`, 'muted')); - } else if (!built || (member.id === 'wall' && !wall) || (member.id === 'para' && !para) || (member.id === 'slab' && !slabR)) { + } else if (!built || (member.id === 'blk' && !blockR) || (member.id === 'wall' && !wall) || (member.id === 'para' && !para) || (member.id === 'slab' && !slabR)) { $('rebar-note').textContent = `${rec.name} · 교대 모델을 만들 수 없어 배근하지 않습니다.`; } else if (member.id === 'wall' && wall) { const g = wall.geom; @@ -3259,6 +3279,11 @@ function renderAbutRebar(rec: AbutRecord): void { const high = g.segments.find((sg) => sg.no === g.segments.length)!; $('rebar-note').textContent = `${rec.name} · Type-1 · 두께 ${Math.round(g.xf - g.xb)} · 높이 ${Math.round(low.H)} (구간 1) ~ ${Math.round(high.H)} (구간 ${high.no}) · 교좌면 계단 ${g.segments.length}구간 · 피복 ${wall.result.settings.c}`; fillAbutWallRebarCard(box, rec, wall); + } else if (member.id === 'blk' && blockR) { + const r = blockR.result; + const nBlocks = new Set(r.paths.map((p) => p.block)).size; + $('rebar-note').textContent = `${rec.name} · Type-1 · 블럭 ${nBlocks}개 (분리) · 1200 × 1200 × ${r.concreteH + ABUT_BLOCK_PAD_MM} (패드 ${ABUT_BLOCK_PAD_MM} 포함) · 묻힘 S2 ${Math.round(r.embedment.S2)} · S1 ${Math.round(r.embedment.S1)}`; + fillAbutBlockRebarCard(box, rec, blockR.result); } else if (member.id === 'slab' && slabR) { const g = slabR.geom; $('rebar-note').textContent = `${rec.name} · Type-1 ${g.form === 'buried' ? '매립형' : '노출형'} · 폭 ${Math.round(g.B)} × 길이 ${Math.round(g.Ls)} · 경사 ${g.gradePct.toFixed(1)} %${g.form === 'exposed' ? ' · 매립형 규칙 적용 (사용자 결정 2026-11-05)' : ''}`; @@ -3586,6 +3611,56 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge }); } +/** ③ 교대 받침블럭 카드 (시안, 받침블럭철근.md §8). 입력 두 칸: 교대축 (S2) · 교대축 직각 (S1) — 사용자 결정 2026-11-05. */ +function fillAbutBlockRebarCard(box: HTMLDivElement, rec: AbutRecord, r: AbutBlockRebar): void { + const s = r.settings; + const D = ABUT_BLOCK_REBAR_DEFAULT; + const C = ABUT_BLOCK_COLORS; + const view = state.abutBlockView; + const seat = (m: 'S1' | 'S2') => `${Math.round(r.embedment[m])}`; + abutRebarCard(box, { + idPrefix: `abk-${rec.name}`, + views: [['plan', '평면'], ['sec', '단면']], + view, + setView: (id) => (state.abutBlockView = id as 'plan' | 'sec'), + svg: (pick) => (view === 'plan' ? abutBlockRebarPlanSvg(r, pick) : abutBlockRebarSectionSvg(r, pick)), + caption: view === 'plan' ? '평면 · 블럭 1200 × 1200 (받침 1개) · S2 실선 (위, 교대축 간격) · S1 파선 (아래, 교대축 직각 간격)' : '단면 · 교좌면 기준 · 몰탈패드 50 · S2 위 줄 · S1 아래 줄 · 다리는 교좌면 아래로 묻힘', + memo: `블럭 ${r.paths.length ? new Set(r.paths.map((p) => p.block)).size : 0}개 (받침마다 1개, 분리). S1 다리 묻힘 ${seat('S1')} — 벽체 W2 층(교좌면 − 100)을 지나면 경고만 (Q-S4, 확인 후 결정).`, + legendHtml: `━ S1 (주철근, 아래) · ━ S2 (보조, 위)`, + baseTitle: '기본간격 · 받침블럭 (부재별 입력, 두 칸)', + base: [ + ['교대축 (mm)', s.spacingS2, D.spacingS2, (v) => ({ spacingS2: v })], + ['교대축 직각 (mm)', s.spacingS1, D.spacingS1, (v) => ({ spacingS1: v })], + ], + inputs: [ + ['상면 피복 (mm)', s.coverTop, D.coverTop, (v) => ({ coverTop: v })], + ['측면 피복 (mm)', s.coverSide, D.coverSide, (v) => ({ coverSide: v })], + ['첫 철근 최소 여유 (mm)', s.firstGap, D.firstGap, (v) => ({ firstGap: v })], + ['S1 · S2 다리 길이 (mm)', s.legLength, D.legLength, (v) => ({ legLength: v })], + ], + autos: [ + ['블럭 콘크리트 높이 (블럭 − 패드 50)', `${r.concreteH} (≥ ${s.coverTop} + 13 → 블럭 안 배근)`], + ['교좌면 아래 묻힘 S2 · S1', `${seat('S2')} · ${seat('S1')}`], + ['적용 블럭', `${r.paths.length ? new Set(r.paths.map((p) => p.block)).size : 0}개 · 배치 분리`], + ], + bars: r.bars.map((b) => ({ mark: b.mark, role: b.role, size: b.size, spacing: b.spacing, actual: b.actual, shape: b.shape, length: b.length, count: b.count, position: b.position, note: b.note, basis: b.basis, modified: b.modified, lengthMin: undefined, lengthMax: b.length })), + roleColor: { 주철근: C.s1, 보조: C.s2 }, + overrides: { + S1: { key: 'spacingS1', base: s.spacingS1, label: 'S1 교대축 직각 간격' }, + S2: { key: 'spacingS2', base: s.spacingS2, label: 'S2 교대축 간격' }, + }, + basisNote: `근거 · 받침블럭철근.md §6.6 · 규격 S1 · S2 H13 · 교각 받침블럭 규칙 재사용 (깊이만 H13) · 묻힘 S2 ${seat('S2')} / S1 ${seat('S1')}`, + set: (patch) => { + try { + applyAbutRecord(applyBlockRebar(rec, patch)); + } catch (err) { + setError((err as Error).message); + renderAll(); + } + }, + }); +} + /** ③ 교대 접속슬래브 카드 (시안, 교대접속슬래브철근.md §9). */ function fillAbutSlabRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutSlabGeom; result: AbutSlabRebar }): void { const { geom: g, result: r } = built;