diff --git a/src/core/changes.ts b/src/core/changes.ts index ed41efb..a38c22d 100644 --- a/src/core/changes.ts +++ b/src/core/changes.ts @@ -23,7 +23,7 @@ export const EDITABLE_FIELDS = [ ] as const; /** Inputs tracked for overrides and pending changes: the numeric cards plus 열별 본수 (an array, compared by value). */ -export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout'] as const; +export const TRACKED_KEYS = [...EDITABLE_FIELDS.map((f) => f.key), 'pileRows', 'groundPosition', 'groundWaterOn', 'groundWaterEl', 'floodLevelEl', 'scourDepthM', 'highWaterEl', 'lowWaterEl', 'strataRows', 'bearingType', 'bearingLayout', 'blockRebar'] as const; export type EditableKey = (typeof TRACKED_KEYS)[number]; @@ -53,6 +53,7 @@ export const FIELD_LABELS: Record = { lowWaterEl: '저조위 LWL', bearingType: '받침 종류·용량', bearingLayout: '받침 배치', + blockRebar: '받침블럭 철근 설정', strataRows: '지층 행', }; diff --git a/src/main.ts b/src/main.ts index 9855e45..cbc1520 100644 --- a/src/main.ts +++ b/src/main.ts @@ -22,6 +22,9 @@ import { bearingEdgeDistances, integratedCheck, PAIR_ECCENTRICITY_LIMIT_MM, pair import { bearingPlanSvg, type BearingPlanOverlay, PAIR_CHIP_LABELS } from './view/bearing-plan-svg'; import { bearingSectionSvg, type BearingDrawingRef } from './view/bearing-section-svg'; import { solplateDetailSvg, type SolplateDrawingRef } from './view/solplate-svg'; +import { blockRebarSectionSvg } from './view/block-rebar-svg'; +import { BLOCK_REBAR_RANGES } from './core/rebar/block-check'; +import { DEFAULT_BLOCK_REBAR, type BlockRebarSettings } from './core/rebar/bearing-block'; import { groundSectionSvg } from './view/ground-svg'; import { parseBearingElevationTable, parseSolplateTable, readDxfTexts } from './core/dxf-table'; import { columnDiameterIssues } from './core/column-check'; @@ -987,6 +990,56 @@ for (const b of document.querySelectorAll('#rebar-color butto }); } +/** 받침블럭 배근 설정 (승인 2026-10-26, 시안 ③): the six inputs (ranges shown), the 적용 블럭, and the C–C section. */ +const BLOCK_REBAR_FIELDS: { key: keyof BlockRebarSettings; label: string }[] = [ + { key: 'coverTop', label: '상면 피복 (철근 중심, mm)' }, + { key: 'coverSide', label: '측면 피복 (mm)' }, + { key: 'firstGap', label: '첫 철근 최소 여유 (mm)' }, + { key: 'spacingS1', label: '간격 S1 (mm)' }, + { key: 'spacingS2', label: '간격 S2 (mm)' }, + { key: 'legLength', label: '다리 길이 (mm)' }, +]; + +function setBlockRebar(name: string, key: keyof BlockRebarSettings, value: number): void { + const p = state.piers.find((q) => q.input.name === name); + if (!p) return; + if (!Number.isFinite(value)) { + setError('받침블럭 철근 값은 숫자로 입력하세요. 값을 되돌렸습니다.'); + renderAll(); + return; + } + p.draft = { ...p.draft, blockRebar: { ...p.draft.blockRebar, [key]: value } }; + regenerateOne(name); +} + +function renderBlockRebarSettings(box: HTMLDivElement, pier: Pier): void { + if (!pier.model) return; + const wrap = el('div', undefined, 'bearing-ref'); + const current: BlockRebarSettings = { ...DEFAULT_BLOCK_REBAR, ...pier.draft.blockRebar }; + wrap.appendChild(el('p', '받침블럭 배근 설정 (기본값 = P1 · 범위는 칸 옆에 표시)', 'lbl')); + const grid = el('div', undefined, 'edit-fields'); + for (const f of BLOCK_REBAR_FIELDS) { + const row = el('div', undefined, 'edit-row'); + const label = el('label', f.label); + const input = el('input'); + input.type = 'number'; + input.step = '1'; + input.value = String(current[f.key]); + input.addEventListener('change', () => setBlockRebar(pier.input.name, f.key, Number(input.value))); + const [lo, hi] = BLOCK_REBAR_RANGES[f.key]; + row.append(label, input, el('span', `범위 ${lo}–${hi} · 기본 ${DEFAULT_BLOCK_REBAR[f.key]}`, 'muted small')); + grid.appendChild(row); + } + wrap.appendChild(grid); + const n = pier.model.integratedBlocks.length; + wrap.appendChild(el('p', `적용 블럭: ${n > 0 ? `일체 ${n}쌍` : `분리 ${pier.model.blocks.length}개`} (전체)`, 'muted small')); + const fig = el('figure', undefined, 'ground-fig'); + fig.innerHTML = blockRebarSectionSvg(pier.model); + fig.appendChild(el('figcaption', '단면 C–C · 교축 방향 · S2 점(상면), S1 선과 다리(⊓) · 치수 mm', 'muted small')); + wrap.appendChild(fig); + box.appendChild(wrap); +} + function renderRebar(): void { const pier = activePier(); // 교각 목록 with the 배근 status (시안 ③). The checks are the same as in ① and ②. @@ -1020,6 +1073,7 @@ function renderRebar(): void { } const { rows, warnings } = rebarSummary(pier.model, state.material, state.fckByMember); $('rebar-note').textContent = `${pier.input.name} · 형상 기준 계산 · 값은 기본값 기준 문서 규칙`; + if (state.rebarMember === 'bearing') renderBlockRebarSettings(box, pier); const shown = rows.filter((r) => r.member === state.rebarMember); box.appendChild(rebarSelectBar(shown.map((r) => r.name))); const table = el('table', undefined, 'grid-table rebar-table'); @@ -1632,6 +1686,7 @@ function valueText(key: EditableKey, v: unknown): string { return rows.length ? rows.map((r) => `${r.name} ${r.bottomEl}`).join(' / ') : '없음'; } if (typeof v === 'boolean') return v ? '예' : '아니오'; + if (typeof v === 'object') return JSON.stringify(v); return String(v); } diff --git a/src/view/block-rebar-svg.ts b/src/view/block-rebar-svg.ts new file mode 100644 index 0000000..42f9787 --- /dev/null +++ b/src/view/block-rebar-svg.ts @@ -0,0 +1,55 @@ +import type { PierModel } from '../core/geometry'; +import { barDepths, barsAcross, type BlockRebarSettings } from '../core/rebar/bearing-block'; +import { INTEGRATED_BLOCK } from '../core/integrated-block'; + +/** + * 받침블럭 배근 · 단면 C–C (승인 2026-10-26, 시안 ③): a section along the 교축, through the blocks. x = 교축 (n), y = EL + * (down from the highest block top). S2 bars are dots along the top, S1 bars are lines with legs down into the coping. + * Dimensions: the block widths and gap, the bar layout (end · n@spacing = span), and the top cover. + */ +export function blockRebarSectionSvg(m: PierModel): string { + const s: BlockRebarSettings = m.blockRebar; + const depth = barDepths(s); + const ib = m.integratedBlocks; + const parts: string[] = []; + const topRef = Math.max(...m.blocks.map((b) => b.top)); + const Y = (el: number) => (topRef - el) * 1000; + + // Spans to draw: one 2500 span per 일체 pair, else one 1200 span per bearing. + const spans = ib.length > 0 + ? ib.map((x) => ({ nc: x.n, width: INTEGRATED_BLOCK.length, bottom: x.bottom, topL: x.topFront, topR: x.topBack, depthTopL: x.topFront, depthTopR: x.topBack, stepped: true })) + : m.blocks.map((b) => ({ nc: b.n, width: 1200, bottom: b.bottom, topL: b.top, topR: b.top, depthTopL: b.top, depthTopR: b.top, stepped: false })); + const nAll = spans.map((x) => x.nc); + const nc = (Math.min(...nAll) + Math.max(...nAll)) / 2; + const X = (n: number) => (n - nc) * 1000; + + for (const sp of spans) { + const x0 = X(sp.nc - sp.width / 2000); + const x1 = X(sp.nc + sp.width / 2000); + if (sp.stepped) { + const xm = X(sp.nc); + parts.push(``); + } else { + parts.push(``); + } + // S2: dots along the top at the S2 depth (each half at its own top when stepped). + const a2 = barsAcross(sp.width, s.spacingS2, s); + for (let i = 0; i < a2.count; i++) { + const xb = x0 + a2.firstEnd + i * a2.spacing; + const top = sp.stepped && xb > (x0 + x1) / 2 ? sp.depthTopR : sp.depthTopL; + parts.push(``); + } + // S1: a line across the block at the S1 depth, legs down into the coping at both ends. + const low = sp.stepped ? Math.min(sp.topL, sp.topR) : sp.topL; + const z1 = low - depth.S1 / 1000; + const leg = s.legLength; + parts.push(``); + parts.push(``); + parts.push(``); + // Dimensions: the S2 layout under the span. + const yDim = Y(sp.bottom) + 120; + parts.push(`${a2.firstEnd.toFixed(1)} · ${a2.count - 1}@${a2.spacing}=${(a2.count - 1) * a2.spacing} · ${a2.firstEnd.toFixed(1)}`); + parts.push(`${(sp.width).toFixed(0)}`); + } + return `${parts.join('')}`; +} diff --git a/tests/block-rebar-svg.test.ts b/tests/block-rebar-svg.test.ts new file mode 100644 index 0000000..dce2fe8 --- /dev/null +++ b/tests/block-rebar-svg.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from 'vitest'; +import { buildPier } from '../src/core/geometry'; +import { blockRebarSectionSvg } from '../src/view/block-rebar-svg'; +import { loadP1 } from './helpers'; + +describe('받침블럭 배근 단면 C–C (승인 2026-10-26)', () => { + it('shows the block widths 1200 and the 98 gap, and the S2 layout 7@125=875 for a separate pier', () => { + const svg = blockRebarSectionSvg(buildPier(loadP1())); + expect(svg).toContain('>1200<'); + expect(svg).toContain('162.5 · 7@125=875 · 162.5'); + }); + + it('a 일체 pier shows the 2500 block and the 19 S2 bars (18@125 = 2250 between the first and last)', () => { + const svg = blockRebarSectionSvg(buildPier({ ...loadP1(), bearingLayout: '일체' })); + expect(svg).toContain('>2500<'); + expect(svg).toContain('@125=2250'); + }); +});