feat(rebar): block rebar settings card and C–C section on the ③ bearing tab
- Six inputs with their ranges and P1 defaults; a change regenerates the pier. - The 적용 블럭 line shows the blocks (분리 n개 or 일체 n쌍, 전체). - view/block-rebar-svg.ts: the section C–C along the 교축 with the S2 dots, the S1 lines and legs, and the dimensions (block widths, the S2 layout). - 받침블럭 철근 설정 is tracked for changes (defaults vs the current draft). Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a6f362df56
commit
38bb26eda5
4 files changed
+130
-1
No files matched your search
+2
-1
@@ -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<EditableKey, string> = {
|
||||
lowWaterEl: '저조위 LWL',
|
||||
bearingType: '받침 종류·용량',
|
||||
bearingLayout: '받침 배치',
|
||||
blockRebar: '받침블럭 철근 설정',
|
||||
strataRows: '지층 행',
|
||||
};
|
||||
|
||||
|
||||
+55
@@ -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<HTMLButtonElement>('#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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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(`<polygon points="${x0},${Y(sp.bottom)} ${x1},${Y(sp.bottom)} ${x1},${Y(sp.topR)} ${xm},${Y(sp.topR)} ${xm},${Y(sp.topL)} ${x0},${Y(sp.topL)}" fill="#D6DDE5" stroke="#6E8496" stroke-width="8"></polygon>`);
|
||||
} else {
|
||||
parts.push(`<rect x="${x0}" y="${Y(sp.topL)}" width="${x1 - x0}" height="${(sp.topL - sp.bottom) * 1000}" fill="#D6DDE5" stroke="#6E8496" stroke-width="8"></rect>`);
|
||||
}
|
||||
// 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(`<circle cx="${xb}" cy="${Y(top - depth.S2 / 1000)}" r="16" fill="#0F5C66"></circle>`);
|
||||
}
|
||||
// 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(`<line x1="${x0 + s.coverSide / 1}" y1="${Y(z1)}" x2="${x1 - s.coverSide / 1}" y2="${Y(z1)}" stroke="#B4531F" stroke-width="10"></line>`);
|
||||
parts.push(`<line x1="${x0 + s.coverSide / 1}" y1="${Y(z1)}" x2="${x0 + s.coverSide / 1}" y2="${Y(z1) + leg}" stroke="#B4531F" stroke-width="10"></line>`);
|
||||
parts.push(`<line x1="${x1 - s.coverSide / 1}" y1="${Y(z1)}" x2="${x1 - s.coverSide / 1}" y2="${Y(z1) + leg}" stroke="#B4531F" stroke-width="10"></line>`);
|
||||
// Dimensions: the S2 layout under the span.
|
||||
const yDim = Y(sp.bottom) + 120;
|
||||
parts.push(`<text x="${(x0 + x1) / 2}" y="${yDim + 90}" text-anchor="middle" font-size="70" fill="#18212B">${a2.firstEnd.toFixed(1)} · ${a2.count - 1}@${a2.spacing}=${(a2.count - 1) * a2.spacing} · ${a2.firstEnd.toFixed(1)}</text>`);
|
||||
parts.push(`<text x="${(x0 + x1) / 2}" y="${Y(topRef) - 80}" text-anchor="middle" font-size="70" fill="#18212B">${(sp.width).toFixed(0)}</text>`);
|
||||
}
|
||||
return `<svg viewBox="-1900 -700 3800 1900" width="100%" style="max-width: 680px; height: auto" role="img" aria-label="받침블럭 배근 단면 C–C: S2 점, S1 선과 다리, 치수">${parts.join('')}</svg>`;
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user