feat(abut-rebar): ③ 교대 흉벽 · 브라켓 카드
- 보기 탭 세 개(단면 · 교대축 단면 · P10 다웰 배치), 입력: 기본간격 2종 · 피복 · P6 · P7 배수(한 칸) · P10 간격 · 길이 - 자동 값: 상단 · 하단 경계, 연직 격자 오프셋, P6 · P7 간격, l_d, P10 개수 · 옮긴 개수. 실간격 묶음 수정 P1–P3 / P4 · P5 · P8 · P9 - 대표 단면 안내 (흉벽 상단선 경사로 흉벽 높이가 위치마다 다름) - 벽체 · 흉벽 카드 공통 함수 abutRebarCard로 정리 (벽체 카드 동작 그대로) Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
914e3dbda8
commit
9f152fef90
1 file changed
+239
-93
+239
-93
@@ -28,9 +28,12 @@ import {
|
||||
setAbutGround,
|
||||
applyFootingRebar as applyAbutFootingRebar,
|
||||
applyWallRebar,
|
||||
applyParapetRebar,
|
||||
} 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_PARAPET_REBAR_DEFAULT, ABUT_PARAPET_SIZES, abutParapetGeom, abutParapetRebar, slabSpanAt, type AbutParapetGeom, type AbutParapetRebar, type AbutParapetRebarSettings } from './core/abut/rebar/parapet';
|
||||
import { ABUT_PARAPET_COLORS, abutParapetDowelSvg, abutParapetElevationSvg, abutParapetSectionSvg } from './view/abut-parapet-rebar-svg';
|
||||
import { ABUT_WALL_REBAR_DEFAULT, ABUT_WALL_SIZES, abutWallGeom, abutWallRebar, type AbutWallGeom, type AbutWallRebar, type AbutWallRebarSettings } from './core/abut/rebar/wall';
|
||||
import { ABUT_WALL_COLORS, abutWallElevationSvg as abutWallRebarElevationSvg, abutWallSectionSvg as abutWallRebarSectionSvg } from './view/abut-wall-rebar-svg';
|
||||
import { lapLength } from './core/abut/rebar/splice';
|
||||
@@ -219,6 +222,8 @@ const state = {
|
||||
abutRebarView: 'w' as 'w' | 'l' | 'plan',
|
||||
/** ③ 교대 벽체 보기 탭 (교대축 직각 단면 / 교대축 단면 계단). */
|
||||
abutWallView: 'sec' as 'sec' | 'elev',
|
||||
/** ③ 교대 흉벽 보기 탭 (단면 / 교대축 단면 / P10 다웰 배치). */
|
||||
abutParapetView: 'sec' as 'sec' | 'elev' | 'dw',
|
||||
abutRebarNotes: false,
|
||||
/** Member card selected on ② (시안 ② 부재 목록). */
|
||||
member: 'coping' as MemberId,
|
||||
@@ -3090,12 +3095,12 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
|
||||
box.appendChild(wrap);
|
||||
}
|
||||
|
||||
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) have rules; the rest come next. */
|
||||
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) have rules; the rest come next. */
|
||||
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: 'wall', label: '벽체', marks: 'W1–W11', ready: true },
|
||||
{ id: 'para', label: '흉벽·브라켓', marks: 'P1–P11', ready: false },
|
||||
{ id: 'para', label: '흉벽·브라켓', marks: 'P1–P11', ready: true },
|
||||
{ id: 'foot', label: '확대기초', marks: 'B1–B10', ready: true },
|
||||
{ id: 'wing', label: '날개벽', marks: 'WR1–WR20', ready: false },
|
||||
{ id: 'slab', label: '접속슬래브', marks: 'R1–R8', ready: false },
|
||||
@@ -3126,11 +3131,31 @@ function abutWallBuilt(rec: AbutRecord, footing: AbutFootingRebar): { geom: Abut
|
||||
}
|
||||
}
|
||||
|
||||
/** ③ with an abutment selected (2026-11-04): 부재 · 교대 list, the member card, and the RB-AF / RB-AB alerts. */
|
||||
/** The 흉벽 · 브라켓 rebar of an abutment, on its 벽체 grid; P10 from the ② 접속슬래브 plan (null when it does not build). */
|
||||
function abutParapetBuilt(rec: AbutRecord, wall: { geom: AbutWallGeom; result: AbutWallRebar }): { geom: AbutParapetGeom; result: AbutParapetRebar } | null {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
const wings = buildWings(model, members, rec.wings, rec.wingDims);
|
||||
const slab = resolveSlab(rec.slab);
|
||||
const tM = (wall.geom.xb / 1000 + members.shape.parapetBackM) / 2;
|
||||
const span = slabSpanAt(abutPlanOutline(model, members, wings, slab).slab, tM, abutSkewK(model));
|
||||
const chamfer = (sEnd: number) => (wings.find((w) => Math.abs(w.sEnd - sEnd) < 1e-9)?.haunchM ?? 0) * 1000;
|
||||
const B = model.wing?.slabWidthMm;
|
||||
const slabGeom = span && B !== undefined ? { B, a: span.a, b: span.b, nearT: members.shape.parapetBackM * 1000 - slab.parapetGapMm, chamferA: chamfer(0), chamferB: chamfer(model.length) } : null;
|
||||
const geom = abutParapetGeom(wall.geom, members.shape, slabGeom);
|
||||
return { geom, result: abutParapetRebar(geom, wall.result, rec.rebar?.parapet, materialFor(state.material, state.fckByMember, 'abutBody'), rec.name) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** ③ with an abutment selected (2026-11-04): 부재 · 교대 list, the member card, and the RB-AF / RB-AB / RB-AP alerts. */
|
||||
function renderAbutRebar(rec: AbutRecord): void {
|
||||
const built = abutFootingBuilt(rec);
|
||||
const wall = built ? abutWallBuilt(rec, built.result) : null;
|
||||
const issuesOf = (id: AbutRebarMemberId) => (id === 'foot' ? built?.result.issues : id === 'wall' ? wall?.result.issues : undefined) ?? [];
|
||||
const para = wall ? abutParapetBuilt(rec, wall) : null;
|
||||
const issuesOf = (id: AbutRebarMemberId) => (id === 'foot' ? built?.result.issues : id === 'wall' ? wall?.result.issues : id === 'para' ? para?.result.issues : undefined) ?? [];
|
||||
const warnOf = (id: AbutRebarMemberId) => issuesOf(id).filter((i) => i.level !== 'I').length;
|
||||
const tabs = $<HTMLUListElement>('rebar-tabs');
|
||||
tabs.replaceChildren();
|
||||
@@ -3159,7 +3184,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)) {
|
||||
} else if (!built || (member.id === 'wall' && !wall) || (member.id === 'para' && !para)) {
|
||||
$('rebar-note').textContent = `${rec.name} · 교대 모델을 만들 수 없어 배근하지 않습니다.`;
|
||||
} else if (member.id === 'wall' && wall) {
|
||||
const g = wall.geom;
|
||||
@@ -3167,6 +3192,10 @@ 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 === 'para' && para) {
|
||||
const g = para.geom;
|
||||
$('rebar-note').textContent = `${rec.name} · Type-1 · 흉벽 두께 ${Math.round(-g.parapetBack)} · 높이 ${Math.round(g.Hp)} (대표 단면) · 브라켓 ${Math.round(g.parapetBack - g.wall.xb)} × ${Math.round(g.bracketH)} · 피복 ${para.result.settings.c}`;
|
||||
fillAbutParapetRebarCard(box, rec, para);
|
||||
} else {
|
||||
const g = built.geom;
|
||||
const toe = Math.round(g.W - g.xf);
|
||||
@@ -3206,20 +3235,52 @@ function abutSelectRow(tr: HTMLTableRowElement, mark: string, togglePick: (mark:
|
||||
return cell;
|
||||
}
|
||||
|
||||
/** ③ 교대 벽체 카드 (시안, 교대벽체철근.md §9): 보기 탭 · 삽도 → 구간 메모 · 범례 → 기본간격 · 입력 → 자동 값 → 철근표 → 근거. */
|
||||
function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutWallGeom; result: AbutWallRebar }): void {
|
||||
const { geom, result: r } = built;
|
||||
const s = r.settings;
|
||||
const D = ABUT_WALL_REBAR_DEFAULT;
|
||||
/** One 철근표 row of a ③ 교대 벽체 · 흉벽 card. */
|
||||
interface AbutCardBar {
|
||||
mark: string;
|
||||
role: string;
|
||||
size: string;
|
||||
spacing: string;
|
||||
actual: string;
|
||||
shape: string;
|
||||
length: number;
|
||||
lengthMin?: number;
|
||||
lengthMax: number;
|
||||
count: number;
|
||||
position: string;
|
||||
note: string;
|
||||
basis: '규칙' | '가정';
|
||||
modified: boolean;
|
||||
}
|
||||
|
||||
/** What a ③ 교대 벽체 · 흉벽 card shows (시안 순서: 보기 탭 · 삽도 → 메모 · 범례 → 기본간격 · 입력 → 자동 값 → 철근표 → 근거). */
|
||||
interface AbutCardSpec<S> {
|
||||
idPrefix: string;
|
||||
views: readonly (readonly [string, string])[];
|
||||
view: string;
|
||||
setView: (id: string) => void;
|
||||
svg: (pick: AbutRebarPick) => string;
|
||||
caption: string;
|
||||
memo?: string;
|
||||
legendHtml: string;
|
||||
baseTitle: string;
|
||||
/** label, value, default, patch. */
|
||||
base: [string, number, number, (v: number) => Partial<S>][];
|
||||
inputs: [string, number, number, (v: number) => Partial<S>][];
|
||||
/** "8 × 4" or "400 · 600" text input of two numbers. */
|
||||
pair: { label: string; value: [number, number]; def: [number, number]; sep: string; error: string; patch: (a: number, b: number) => Partial<S> };
|
||||
autos: [string, string][];
|
||||
bars: AbutCardBar[];
|
||||
roleColor: Record<string, string>;
|
||||
/** 묶음별 간격 수정: the row that edits a group, its key, the 기본간격-linked value, and the label. */
|
||||
overrides: Partial<Record<string, { key: keyof S; base: number; label: string }>>;
|
||||
basisNote: string;
|
||||
set: (patch: Partial<S>) => void;
|
||||
}
|
||||
|
||||
/** ③ 교대 벽체 · 흉벽 카드 공통 (2026-11-04). The selection is the ③ rebarSelection (표 · 삽도 · 3D 공통). */
|
||||
function abutRebarCard<S>(box: HTMLDivElement, o: AbutCardSpec<S>): void {
|
||||
const pick: AbutRebarPick = { marks: state.rebarSelection, only: state.rebarIsolate };
|
||||
const set = (patch: Partial<AbutWallRebarSettings>) => {
|
||||
try {
|
||||
applyAbutRecord(applyWallRebar(rec, patch));
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
renderAll();
|
||||
}
|
||||
};
|
||||
const togglePick = (mark: string) => {
|
||||
const alone = state.rebarSelection.size === 1 && state.rebarSelection.has(mark);
|
||||
state.rebarSelection = alone ? new Set() : new Set([mark]);
|
||||
@@ -3227,18 +3288,18 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
};
|
||||
// 보기 탭.
|
||||
const head = el('div', undefined, 'abut-rebar-head');
|
||||
for (const [id, label] of [['sec', '교대축 직각 단면'], ['elev', '교대축 단면 (계단)']] as const) {
|
||||
const b = el('button', label, `abut-optt${state.abutWallView === id ? ' on' : ''}`);
|
||||
for (const [id, label] of o.views) {
|
||||
const b = el('button', label, `abut-optt${o.view === id ? ' on' : ''}`);
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(state.abutWallView === id));
|
||||
b.setAttribute('aria-pressed', String(o.view === id));
|
||||
b.addEventListener('click', () => {
|
||||
state.abutWallView = id;
|
||||
o.setView(id);
|
||||
renderAll();
|
||||
});
|
||||
head.append(b);
|
||||
}
|
||||
box.append(head);
|
||||
const picked = r.bars.filter((b) => state.rebarSelection.has(b.mark));
|
||||
const picked = o.bars.filter((b) => state.rebarSelection.has(b.mark));
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
@@ -3249,7 +3310,7 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
),
|
||||
);
|
||||
const fig = el('figure', undefined, 'ground-fig abut-rebar-fig');
|
||||
fig.innerHTML = state.abutWallView === 'sec' ? abutWallRebarSectionSvg(geom, r, pick) : abutWallRebarElevationSvg(geom, r, pick);
|
||||
fig.innerHTML = o.svg(pick);
|
||||
fig.querySelectorAll<SVGGElement>('[data-pick]').forEach((g) => {
|
||||
const mark = g.getAttribute('data-pick')!;
|
||||
g.addEventListener('click', () => togglePick(mark));
|
||||
@@ -3260,32 +3321,16 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
}
|
||||
});
|
||||
});
|
||||
fig.append(
|
||||
el(
|
||||
'figcaption',
|
||||
state.abutWallView === 'sec'
|
||||
? '교대축 직각 단면 · 구간 1 (IP3 끝) · 왼쪽 배면 → 오른쪽 전면 · 선 = 연직 · W2 · W7, 점 = W5 · W6 · W9 · W10'
|
||||
: '교대축 단면 · 전면 철근 · 왼쪽 IP3 = 구간 1 → 오른쪽 IP1 · W5는 계단에서 끊김, W9 · W10은 구간마다 ⊓ (양끝 경계 − 피복에서 다리 l_d)',
|
||||
'muted small',
|
||||
),
|
||||
);
|
||||
fig.append(el('figcaption', o.caption, 'muted small'));
|
||||
box.append(fig);
|
||||
// 구간 메모 · 범례 (시안).
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
`벽체 높이 구간 ${[...geom.segments].sort((a, b) => a.no - b.no).map((sg) => `${sg.no} ${Math.round(sg.H)}`).join(' · ')} · 수평 W5는 높이가 들어가는 구간까지만 (위 계단 면 − ${lenText(s.c)}에서 끊음) · W9 · W10은 구간마다 ⊓, 양끝 다리 l_d ${r.ld.H16}`,
|
||||
'muted small',
|
||||
),
|
||||
);
|
||||
const C = ABUT_WALL_COLORS;
|
||||
if (o.memo) box.append(el('p', o.memo, 'muted small'));
|
||||
const legend = el('p', undefined, 'muted small abut-rebar-legend');
|
||||
legend.innerHTML = `<span style="color:${C.vertical}">━ 연직 (W1 · W3 · W4 · W11)</span> · <span style="color:${C.horizontal}">━ 수평 · 교좌면 (W2 · W5 · W6 · W8)</span> · <span style="color:${C.step}">━ 계단 상면 (W9 실선 · W10 파선)</span> · <span style="color:${C.tie}">┅ 간격재 W7</span>`;
|
||||
legend.innerHTML = o.legendHtml;
|
||||
box.append(legend);
|
||||
|
||||
// 입력: 기본간격 · 피복 · 첫 철근 최소 여유 · W7 배수 (사용자 결정 2026-11-04: W7 배수 편집, e_min 표시).
|
||||
const num = (into: HTMLElement, label: string, value: number, apply: (v: number) => Partial<AbutWallRebarSettings>, def: number) => {
|
||||
const id = `awr-${rec.name}-${label}`;
|
||||
// 입력.
|
||||
const num = (into: HTMLElement, [label, value, def, patch]: [string, number, number, (v: number) => Partial<S>]) => {
|
||||
const id = `${o.idPrefix}-${label}`;
|
||||
const lab = el('label', label);
|
||||
lab.htmlFor = id;
|
||||
const input = el('input');
|
||||
@@ -3294,59 +3339,50 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
input.step = 'any';
|
||||
input.value = String(value);
|
||||
if (value !== def) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => set(apply(Number(input.value))));
|
||||
input.addEventListener('change', () => o.set(patch(Number(input.value))));
|
||||
const cell = el('div');
|
||||
cell.append(lab, input);
|
||||
into.append(cell);
|
||||
};
|
||||
const grid = el('div', undefined, 'abut-rebar-inputs');
|
||||
grid.append(el('p', '기본간격 · 벽체 (부재별 입력)', 'lbl'));
|
||||
num(grid, '교대축 (mm)', s.sL, (v) => ({ sL: v }), D.sL);
|
||||
num(grid, '교대축 직각 (mm)', s.sW, (v) => ({ sW: v }), D.sW);
|
||||
num(grid, '연직 (mm)', s.sV, (v) => ({ sV: v }), D.sV);
|
||||
grid.append(el('p', o.baseTitle, 'lbl'));
|
||||
for (const f of o.base) num(grid, f);
|
||||
box.append(grid);
|
||||
const grid2 = el('div', undefined, 'abut-rebar-inputs');
|
||||
num(grid2, '피복 (mm)', s.c, (v) => ({ c: v }), D.c);
|
||||
num(grid2, '첫 철근 최소 여유 (mm)', s.eMin, (v) => ({ eMin: v }), D.eMin);
|
||||
for (const f of o.inputs) num(grid2, f);
|
||||
{
|
||||
const id = `awr-${rec.name}-k7`;
|
||||
const lab = el('label', '간격재 W7 배수 (교대축 × 연직)');
|
||||
const p = o.pair;
|
||||
const id = `${o.idPrefix}-pair`;
|
||||
const lab = el('label', p.label);
|
||||
lab.htmlFor = id;
|
||||
const input = el('input');
|
||||
input.id = id;
|
||||
input.type = 'text';
|
||||
input.value = `${s.k7L} × ${s.k7V}`;
|
||||
if (s.k7L !== D.k7L || s.k7V !== D.k7V) input.classList.add('changed-input');
|
||||
input.value = `${p.value[0]} ${p.sep} ${p.value[1]}`;
|
||||
if (p.value[0] !== p.def[0] || p.value[1] !== p.def[1]) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => {
|
||||
const n = input.value.split(/[×x*,\s]+/).filter(Boolean).map(Number);
|
||||
if (n.length !== 2) {
|
||||
setError('W7 배수는 "8 × 4"처럼 두 정수로 입력하세요.');
|
||||
const n = input.value.split(/[×x*·,\s]+/).filter(Boolean).map(Number);
|
||||
if (n.length !== 2 || n.some((v) => !Number.isFinite(v))) {
|
||||
setError(p.error);
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
set({ k7L: n[0], k7V: n[1] });
|
||||
o.set(p.patch(n[0], n[1]));
|
||||
});
|
||||
const cell = el('div');
|
||||
cell.append(lab, input);
|
||||
grid2.append(cell);
|
||||
}
|
||||
// 자동 값.
|
||||
const fck = materialFor(state.material, state.fckByMember, 'abutBody').fck;
|
||||
const auto = (label: string, value: string) => {
|
||||
for (const [label, value] of o.autos) {
|
||||
const cell = el('div', undefined, 'abut-rebar-auto');
|
||||
cell.append(el('span', label, 'muted small'), el('span', value, 'num'));
|
||||
grid2.append(cell);
|
||||
};
|
||||
const w7 = r.bars.find((b) => b.mark === 'W7');
|
||||
auto('연직철근 격자 (저판 B1 · B3 피함)', `저판 격자 − ${lenText(r.vertical.shift)}`);
|
||||
auto('W7 간격', w7 ? `@${w7.spacing.replace(' × ', ' × @')}` : '—');
|
||||
auto('교대축 직각 철근 방향', '① skew 고정');
|
||||
auto(`l_d (fck ${fck})`, `H13 ${r.ld.H13} · H16 ${r.ld.H16} · H19 ${r.ld.H19}`);
|
||||
}
|
||||
box.append(grid2);
|
||||
|
||||
// 철근표.
|
||||
const bar = el('div', undefined, 'abut-rebar-tablehead');
|
||||
const assumed = r.bars.filter((b) => b.basis === '가정').map((b) => b.mark);
|
||||
const assumed = o.bars.filter((b) => b.basis === '가정').map((b) => b.mark);
|
||||
bar.append(el('span', assumed.length ? `가정값 ${assumed.length}개 (${assumed.join(' · ')}) · 나머지는 규칙 계산` : '가정값 없음 · 모든 철근이 규칙 계산', 'muted small'));
|
||||
const notes = el('button', state.abutRebarNotes ? '비고 접기' : '비고 모두 펼치기', 'btn small');
|
||||
notes.type = 'button';
|
||||
@@ -3356,26 +3392,19 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
});
|
||||
bar.append(notes);
|
||||
box.append(bar);
|
||||
box.append(rebarSelectBar(r.bars.map((b) => b.mark)));
|
||||
box.append(rebarSelectBar(o.bars.map((b) => b.mark)));
|
||||
const table = el('table', undefined, 'grid-table rebar-table');
|
||||
const hr = table.createTHead().insertRow();
|
||||
for (const h of ['선택', '구분', '번호', '규격', '간격', '실간격', '형상', '길이 (mm) · 근거', '개수', '위치']) hr.append(el('th', h));
|
||||
const body = table.createTBody();
|
||||
const roleColor: Record<string, string> = { 연직: C.vertical, 수평: C.horizontal, 간격재: C.tie, '계단 상면': C.step };
|
||||
// 묶음별 간격 수정 (사용자 결정 2026-11-04): W3 · W4 (교대축), W5 · W6 · W8 (연직), W9 · W10 (폭). W1 · W2는 연직 격자를 따름.
|
||||
const overrideOf: Partial<Record<string, { key: 'sVert' | 'sHoriz' | 'sTop'; base: number; label: string }>> = {
|
||||
W3: { key: 'sVert', base: s.sL, label: 'W1–W4 · W2 교대축 실간격' },
|
||||
W5: { key: 'sHoriz', base: s.sV, label: 'W5 · W6 · W8 연직 간격' },
|
||||
W9: { key: 'sTop', base: 2 * s.sW, label: 'W9 · W10 폭 간격' },
|
||||
};
|
||||
for (const b of r.bars) {
|
||||
for (const b of o.bars) {
|
||||
const on = state.rebarSelection.has(b.mark);
|
||||
const tr = body.insertRow();
|
||||
const checkCell = abutSelectRow(tr, b.mark, togglePick);
|
||||
const role = el('td');
|
||||
role.innerHTML = `<span style="display:inline-block;width:22px;border-top:3px solid ${roleColor[b.role]};vertical-align:middle"></span> ${b.role}`;
|
||||
role.innerHTML = `<span style="display:inline-block;width:22px;border-top:3px solid ${o.roleColor[b.role]};vertical-align:middle"></span> ${b.role}`;
|
||||
const actual = el('td');
|
||||
const ov = overrideOf[b.mark];
|
||||
const ov = o.overrides[b.mark];
|
||||
if (ov) {
|
||||
const input = el('input');
|
||||
input.type = 'number';
|
||||
@@ -3384,7 +3413,7 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
input.style.width = '70px';
|
||||
input.setAttribute('aria-label', ov.label);
|
||||
input.addEventListener('click', (ev) => ev.stopPropagation());
|
||||
input.addEventListener('change', () => set({ [ov.key]: Number(input.value) === ov.base ? undefined : Number(input.value) }));
|
||||
input.addEventListener('change', () => o.set({ [ov.key]: Number(input.value) === ov.base ? undefined : Number(input.value) } as Partial<S>));
|
||||
actual.append(input);
|
||||
if (b.modified) {
|
||||
actual.append(el('span', ' 수정됨', 'muted small abut-note-warn'));
|
||||
@@ -3392,15 +3421,17 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
back.type = 'button';
|
||||
back.addEventListener('click', (ev) => {
|
||||
ev.stopPropagation();
|
||||
set({ [ov.key]: undefined });
|
||||
o.set({ [ov.key]: undefined } as Partial<S>);
|
||||
});
|
||||
actual.append(back);
|
||||
}
|
||||
} else {
|
||||
actual.textContent = b.actual + (b.modified && b.mark !== 'W7' ? ' · 수정됨' : '');
|
||||
actual.textContent = b.actual + (b.modified && b.actual !== '—' ? ' · 수정됨' : '');
|
||||
}
|
||||
const len = el('td');
|
||||
len.append(`${b.length}${b.lengthMax !== b.length ? ` ~ ${b.lengthMax}` : ''} `, el('span', b.basis, b.basis === '가정' ? 'badge warn' : 'badge'));
|
||||
// 벽체: 대표 ~ 최대 (구간마다). 흉벽: 대표 (최소 ~ 최대) (상단 경사).
|
||||
const range = b.lengthMin === undefined ? (b.lengthMax !== b.length ? ` ~ ${b.lengthMax}` : '') : b.lengthMin !== b.lengthMax ? ` (${b.lengthMin} ~ ${b.lengthMax})` : '';
|
||||
len.append(`${b.length}${range} `, el('span', b.basis, b.basis === '가정' ? 'badge warn' : 'badge'));
|
||||
tr.append(checkCell, role, el('td', b.mark, 'bar-no'), el('td', b.size), el('td', b.spacing), actual, el('td', b.shape), len, el('td', String(b.count)), el('td', b.position));
|
||||
if (state.abutRebarNotes || on) {
|
||||
const nr = body.insertRow();
|
||||
@@ -3412,15 +3443,130 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
const scroll = el('div', undefined, 'table-scroll');
|
||||
scroll.append(table);
|
||||
box.append(scroll);
|
||||
box.append(el('p', o.basisNote, 'muted small'));
|
||||
}
|
||||
|
||||
/** ③ 교대 벽체 카드 (시안, 교대벽체철근.md §9). */
|
||||
function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutWallGeom; result: AbutWallRebar }): void {
|
||||
const { geom, result: r } = built;
|
||||
const s = r.settings;
|
||||
const D = ABUT_WALL_REBAR_DEFAULT;
|
||||
const C = ABUT_WALL_COLORS;
|
||||
const SZ = ABUT_WALL_SIZES;
|
||||
const fck = materialFor(state.material, state.fckByMember, 'abutBody').fck;
|
||||
const w7 = r.bars.find((b) => b.mark === 'W7');
|
||||
const lv = r.levels.find((l) => l.no === 1);
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
`근거 · 교대벽체철근.md · spec_abut.md §6.3 · 규격 W1 · W3 · W4 · W5 ${SZ.W1} / W2 · W6 · W9 · W10 · W11 ${SZ.W2} / W7 · W8 ${SZ.W7} · 교대축 ${r.vertical.text}${lv ? ` · 삽도 재현: 구간 1 "${lv.text.split(' · ')[0]}"` : ''}`,
|
||||
'muted small',
|
||||
),
|
||||
);
|
||||
abutRebarCard<AbutWallRebarSettings>(box, {
|
||||
idPrefix: `awr-${rec.name}`,
|
||||
views: [['sec', '교대축 직각 단면'], ['elev', '교대축 단면 (계단)']],
|
||||
view: state.abutWallView,
|
||||
setView: (id) => (state.abutWallView = id as 'sec' | 'elev'),
|
||||
svg: (pick) => (state.abutWallView === 'sec' ? abutWallRebarSectionSvg(geom, r, pick) : abutWallRebarElevationSvg(geom, r, pick)),
|
||||
caption:
|
||||
state.abutWallView === 'sec'
|
||||
? '교대축 직각 단면 · 구간 1 (IP3 끝) · 왼쪽 배면 → 오른쪽 전면 · 선 = 연직 · W2 · W7, 점 = W5 · W6 · W9 · W10'
|
||||
: '교대축 단면 · 전면 철근 · 왼쪽 IP3 = 구간 1 → 오른쪽 IP1 · W5는 계단에서 끊김, W9 · W10은 구간마다 ⊓ (양끝 경계 − 피복에서 다리 l_d)',
|
||||
memo: `벽체 높이 구간 ${[...geom.segments].sort((a, b) => a.no - b.no).map((sg) => `${sg.no} ${Math.round(sg.H)}`).join(' · ')} · 수평 W5는 높이가 들어가는 구간까지만 (위 계단 면 − ${lenText(s.c)}에서 끊음) · W9 · W10은 구간마다 ⊓, 양끝 다리 l_d ${r.ld.H16}`,
|
||||
legendHtml: `<span style="color:${C.vertical}">━ 연직 (W1 · W3 · W4 · W11)</span> · <span style="color:${C.horizontal}">━ 수평 · 교좌면 (W2 · W5 · W6 · W8)</span> · <span style="color:${C.step}">━ 계단 상면 (W9 실선 · W10 파선)</span> · <span style="color:${C.tie}">┅ 간격재 W7</span>`,
|
||||
baseTitle: '기본간격 · 벽체 (부재별 입력)',
|
||||
base: [
|
||||
['교대축 (mm)', s.sL, D.sL, (v) => ({ sL: v })],
|
||||
['교대축 직각 (mm)', s.sW, D.sW, (v) => ({ sW: v })],
|
||||
['연직 (mm)', s.sV, D.sV, (v) => ({ sV: v })],
|
||||
],
|
||||
// 사용자 결정 2026-11-04: W7 배수 편집, e_min 표시.
|
||||
inputs: [
|
||||
['피복 (mm)', s.c, D.c, (v) => ({ c: v })],
|
||||
['첫 철근 최소 여유 (mm)', s.eMin, D.eMin, (v) => ({ eMin: v })],
|
||||
],
|
||||
pair: { label: '간격재 W7 배수 (교대축 × 연직)', value: [s.k7L, s.k7V], def: [D.k7L, D.k7V], sep: '×', error: 'W7 배수는 "8 × 4"처럼 두 정수로 입력하세요.', patch: (a, b) => ({ k7L: a, k7V: b }) },
|
||||
autos: [
|
||||
['연직철근 격자 (저판 B1 · B3 피함)', `저판 격자 − ${lenText(r.vertical.shift)}`],
|
||||
['W7 간격', w7 ? `@${w7.spacing.replace(' × ', ' × @')}` : '—'],
|
||||
['교대축 직각 철근 방향', '① skew 고정'],
|
||||
[`l_d (fck ${fck})`, `H13 ${r.ld.H13} · H16 ${r.ld.H16} · H19 ${r.ld.H19}`],
|
||||
],
|
||||
bars: r.bars,
|
||||
roleColor: { 연직: C.vertical, 수평: C.horizontal, 간격재: C.tie, '계단 상면': C.step },
|
||||
// 묶음별 간격 수정 (사용자 결정 2026-11-04): W3 · W4 (교대축), W5 · W6 · W8 (연직), W9 · W10 (폭). W1 · W2는 연직 격자를 따름.
|
||||
overrides: {
|
||||
W3: { key: 'sVert', base: s.sL, label: 'W1–W4 · W2 교대축 실간격' },
|
||||
W5: { key: 'sHoriz', base: s.sV, label: 'W5 · W6 · W8 연직 간격' },
|
||||
W9: { key: 'sTop', base: 2 * s.sW, label: 'W9 · W10 폭 간격' },
|
||||
},
|
||||
basisNote: `근거 · 교대벽체철근.md · spec_abut.md §6.3 · 규격 W1 · W3 · W4 · W5 ${SZ.W1} / W2 · W6 · W9 · W10 · W11 ${SZ.W2} / W7 · W8 ${SZ.W7} · 교대축 ${r.vertical.text}${lv ? ` · 삽도 재현: 구간 1 "${lv.text.split(' · ')[0]}"` : ''}`,
|
||||
set: (patch) => {
|
||||
try {
|
||||
applyAbutRecord(applyWallRebar(rec, patch));
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
renderAll();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** ③ 교대 흉벽 · 브라켓 카드 (시안, 교대흉벽철근.md §10). */
|
||||
function fillAbutParapetRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutParapetGeom; result: AbutParapetRebar }): void {
|
||||
const { geom, result: r } = built;
|
||||
const s = r.settings;
|
||||
const D = ABUT_PARAPET_REBAR_DEFAULT;
|
||||
const C = ABUT_PARAPET_COLORS;
|
||||
const SZ = ABUT_PARAPET_SIZES;
|
||||
const fck = materialFor(state.material, state.fckByMember, 'abutBody').fck;
|
||||
const p6 = r.bars.find((b) => b.mark === 'P6');
|
||||
const off = (v: number) => (v >= 0 ? `+ ${lenText(v)}` : `− ${lenText(-v)}`);
|
||||
const view = state.abutParapetView;
|
||||
abutRebarCard<AbutParapetRebarSettings>(box, {
|
||||
idPrefix: `apr-${rec.name}`,
|
||||
// 사용자 결정 2026-11-04: 시안 두 탭에 교대축 단면을 더해 세 탭.
|
||||
views: [['sec', '단면'], ['elev', '교대축 단면'], ['dw', 'P10 다웰 배치']],
|
||||
view,
|
||||
setView: (id) => (state.abutParapetView = id as 'sec' | 'elev' | 'dw'),
|
||||
svg: (pick) => (view === 'sec' ? abutParapetSectionSvg(geom, r, pick) : view === 'elev' ? abutParapetElevationSvg(geom, r, pick) : abutParapetDowelSvg(geom, r, pick)),
|
||||
caption:
|
||||
view === 'sec'
|
||||
? `교대축 직각 단면 · 대표 단면 (흉벽 높이 ${lenText(geom.Hp)}) · 왼쪽 브라켓 배면 → 오른쪽 흉벽 전면`
|
||||
: view === 'elev'
|
||||
? '교대축 단면 · 전면 · 왼쪽 IP3 → 오른쪽 IP1 · 수평 P4는 흉벽 상단 경사와 나란함, 끝면 P9 (위) · P8 (아래) · P11'
|
||||
: `P10 다웰 배치 · 평면 (접속슬래브 폭 ${geom.slab ? Math.round(geom.slab.B) : '—'}) · 다웰이 P3 수평 다리와 겹치면 가장 가까운 자리로 옮김 (Q-P5)`,
|
||||
memo: `흉벽 상단선은 받침 ① · 마지막 받침으로 정해져 흉벽 높이가 위치마다 다릅니다 (IP1 ${Math.round(geom.top[0] - (geom.wall.HF + geom.wall.segments[0].H))} · IP3 ${Math.round(geom.top[1] - (geom.wall.HF + geom.wall.segments[geom.wall.segments.length - 1].H))}). 철근표 대표 길이 · 단 수는 흉벽 높이 ${lenText(geom.Hp)} 단면, 괄호는 범위입니다.`,
|
||||
legendHtml: `<span style="color:${C.vertical}">━ 연직 (P1–P3 · P11)</span> · <span style="color:${C.horizontal}">━ 수평 (P4 · P5)</span> · <span style="color:${C.tie}">┅ 간격재 (P6 · P7)</span> · <span style="color:${C.side}">┅ 측면 (P8 · P9) · 다웰 P10</span>`,
|
||||
baseTitle: '기본간격 · 흉벽 (부재별 입력) · 교대축 직각은 두지 않음',
|
||||
base: [
|
||||
['교대축 (mm)', s.sL, D.sL, (v) => ({ sL: v })],
|
||||
['연직 (mm)', s.sV, D.sV, (v) => ({ sV: v })],
|
||||
],
|
||||
inputs: [
|
||||
['피복 (mm)', s.c, D.c, (v) => ({ c: v })],
|
||||
['간격재 P6 · P7 배수 · 교대축', s.k67L, D.k67L, (v) => ({ k67L: v })],
|
||||
['간격재 P6 · P7 배수 · 연직', s.k67V, D.k67V, (v) => ({ k67V: v })],
|
||||
],
|
||||
pair: { label: 'P10 간격 · 길이 (기본간격과 별도)', value: [s.s10, s.L10], def: [D.s10, D.L10], sep: '·', error: 'P10 간격 · 길이는 "400 · 600"처럼 두 숫자로 입력하세요.', patch: (a, b) => ({ s10: a, L10: b }) },
|
||||
autos: [
|
||||
['상단 · 하단 경계', `브라켓 상면 (흉벽 상단 − ${lenText(geom.bracketH)})`],
|
||||
['연직 격자 (저판 기준)', `P1 ${off(r.offsets.P1)} · P2 ${off(r.offsets.P2)} · P3 ${off(r.offsets.P3)}`],
|
||||
['P6 · P7 간격', p6 ? `@${p6.spacing.replace(' × ', ' × @')}` : '—'],
|
||||
[`l_d (fck ${fck})`, `H13 ${r.ld.H13} · H16 ${r.ld.H16}`],
|
||||
['P10', `${r.dowel.n}개 · 양끝 여유 ${lenText(r.settings.c + r.dowel.e)}${r.dowel.moved ? ` · ${r.dowel.moved}개 P3 비켜 옮김` : ''}`],
|
||||
],
|
||||
bars: r.bars,
|
||||
roleColor: { 연직: C.vertical, 수평: C.horizontal, 간격재: C.tie, 측면: C.side, 다웰: C.side },
|
||||
// 묶음별 간격 수정 (사용자 결정 2026-11-04): P1–P3 (교대축), P4 · P5 · P8 · P9 (연직). P10은 별도 입력, P11은 흉벽 선.
|
||||
overrides: {
|
||||
P2: { key: 'sVert', base: s.sL, label: 'P1–P3 교대축 실간격' },
|
||||
P4: { key: 'sHoriz', base: s.sV, label: 'P4 · P5 · P8 · P9 연직 간격' },
|
||||
},
|
||||
basisNote: `근거 · 교대흉벽철근.md · spec_abut.md §6.4 · 규격 P1–P5 · P11 ${SZ.P1} / P6–P9 ${SZ.P6} / P10 ${SZ.P10} · 흉벽 연직철근 격자 = 저판 격자 ${off(r.offsets.P1)} (P1) / ${off(r.offsets.P2)} (P2) / ${off(r.offsets.P3)} (P3)`,
|
||||
set: (patch) => {
|
||||
try {
|
||||
applyAbutRecord(applyParapetRebar(rec, patch));
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
renderAll();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** ③ 교대 확대기초 카드 (시안, 교대확대기초철근.md §10): 보기 탭 · 삽도 → 범례 → 기본간격 · 배근 방향 · 입력 → 자동 값 → 철근표 → 근거. */
|
||||
|
||||
Reference in new issue
Block a user