feat(abut-rebar): ③ 교대 날개벽 카드

- 측 선택 (좌 · 우, ② 날개벽 카드와 같은 측), 보기 탭 세 개 (전면도 · 배면도 · 헌치 · 모서리 평면)
- 입력 (좌우 공통): 기본간격 수평 · 연직, 피복, 첫 철근 최소 여유, δ, WR20 배수. WR9 개수는 없앰 (자동)
- 자동 값: 형식, 배면 s · 전면 2s, 연직 기준선, 수평 단, WR9 개수, l_d. 실간격 묶음 수정 WR1 (연직) · WR3 (수평)
- 공통 카드 함수: 위쪽 컨트롤 줄 · 두 숫자 입력 선택 사항

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-04 18:00:41 +09:00
1 parent d15b237100
commit bac087ce63
1 file changed
+122 -6
+122 -6
View File
@@ -29,9 +29,12 @@ import {
applyFootingRebar as applyAbutFootingRebar,
applyWallRebar,
applyParapetRebar,
applyWingRebar,
} 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_WING_REBAR_DEFAULT, ABUT_WING_SIZES, abutWingGeom, abutWingRebar, type AbutWingGeom, type AbutWingRebar, type AbutWingRebarSettings } from './core/abut/rebar/wing';
import { ABUT_WING_COLORS, abutWingCornerSvg as abutWingRebarCornerSvg, abutWingFaceSvg } from './view/abut-wing-rebar-svg';
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';
@@ -224,6 +227,8 @@ const state = {
abutWallView: 'sec' as 'sec' | 'elev',
/** ③ 교대 흉벽 보기 탭 (단면 / 교대축 단면 / P10 다웰 배치). */
abutParapetView: 'sec' as 'sec' | 'elev' | 'dw',
/** ③ 교대 날개벽 보기 탭 (전면도 / 배면도 / 헌치 · 모서리 평면). 측은 ② 날개벽 카드와 같은 abutWingSide. */
abutWingView: 'front' as 'front' | 'back' | 'corner',
abutRebarNotes: false,
/** Member card selected on ② (시안 ② 부재 목록). */
member: 'coping' as MemberId,
@@ -3096,14 +3101,14 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
box.appendChild(wrap);
}
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) have rules; the rest come next. */
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) · 날개벽 (AD7) 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: true },
{ id: 'foot', label: '확대기초', marks: 'B1–B10', ready: true },
{ id: 'wing', label: '날개벽', marks: 'WR1–WR20', ready: false },
{ id: 'wing', label: '날개벽', marks: 'WR1–WR20', ready: true },
{ id: 'slab', label: '접속슬래브', marks: 'R1–R8', ready: false },
];
@@ -3151,12 +3156,29 @@ function abutParapetBuilt(rec: AbutRecord, wall: { geom: AbutWallGeom; result: A
}
}
/** ③ with an abutment selected (2026-11-04): 부재 · 교대 list, the member card, and the RB-AF / RB-AB / RB-AP alerts. */
/** The 날개벽 rebar of an abutment, per side that has a wing (좌우 공통 입력). */
function abutWingsBuilt(rec: AbutRecord, footing: AbutFootingRebar): { geom: AbutWingGeom; result: AbutWingRebar }[] {
try {
const model = buildAbut(toAbutInput(rec));
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
const material = materialFor(state.material, state.fckByMember, 'abutBody');
return buildWings(model, members, rec.wings, rec.wingDims).map((w) => {
const geom = abutWingGeom(model, members, w, footing.z.B68);
return { geom, result: abutWingRebar(geom, rec.rebar?.wing, material, rec.name) };
});
} catch {
return [];
}
}
/** ③ with an abutment selected (2026-11-04): 부재 · 교대 list, the member card, and the RB-AF / RB-AB / RB-AP / RB-AW alerts. */
function renderAbutRebar(rec: AbutRecord): void {
const built = abutFootingBuilt(rec);
const wall = built ? abutWallBuilt(rec, built.result) : null;
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 wings = built ? abutWingsBuilt(rec, built.result) : [];
const issuesOf = (id: AbutRebarMemberId) =>
(id === 'foot' ? built?.result.issues : id === 'wall' ? wall?.result.issues : id === 'para' ? para?.result.issues : id === 'wing' ? wings.flatMap((w) => w.result.issues) : undefined) ?? [];
const warnOf = (id: AbutRebarMemberId) => issuesOf(id).filter((i) => i.level !== 'I').length;
const tabs = $<HTMLUListElement>('rebar-tabs');
tabs.replaceChildren();
@@ -3193,6 +3215,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 === 'wing') {
const forms = rec.wings ?? { left: 'general', right: 'general' };
$('rebar-note').textContent = `${rec.name} · Type-1 · 좌 ${WING_FORM_TEXT[forms.left]} · 우 ${WING_FORM_TEXT[forms.right]}`;
fillAbutWingRebarCard(box, rec, wings);
} 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}`;
@@ -3257,6 +3283,8 @@ interface AbutCardBar {
/** What a ③ 교대 벽체 · 흉벽 card shows (시안 순서: 보기 탭 · 삽도 → 메모 · 범례 → 기본간격 · 입력 → 자동 값 → 철근표 → 근거). */
interface AbutCardSpec<S> {
idPrefix: string;
/** A control row above the 보기 탭 (날개벽: 측 선택). */
headExtra?: HTMLElement;
views: readonly (readonly [string, string])[];
view: string;
setView: (id: string) => void;
@@ -3269,7 +3297,7 @@ interface AbutCardSpec<S> {
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> };
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>;
@@ -3287,6 +3315,7 @@ function abutRebarCard<S>(box: HTMLDivElement, o: AbutCardSpec<S>): void {
state.rebarSelection = alone ? new Set() : new Set([mark]);
renderAll();
};
if (o.headExtra) box.append(o.headExtra);
// 보기 탭.
const head = el('div', undefined, 'abut-rebar-head');
for (const [id, label] of o.views) {
@@ -3351,7 +3380,7 @@ function abutRebarCard<S>(box: HTMLDivElement, o: AbutCardSpec<S>): void {
box.append(grid);
const grid2 = el('div', undefined, 'abut-rebar-inputs');
for (const f of o.inputs) num(grid2, f);
{
if (o.pair) {
const p = o.pair;
const id = `${o.idPrefix}-pair`;
const lab = el('label', p.label);
@@ -3509,6 +3538,93 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
});
}
const WING_FORM_TEXT: Record<string, string> = { general: '일반형', short: '단축형', none: '없음' };
/** ③ 교대 날개벽 카드 (시안, 교대날개벽철근.md §10): 측 선택 + 공통 카드. 입력은 좌우 공통 (사용자 결정 2026-11-04). */
function fillAbutWingRebarCard(box: HTMLDivElement, rec: AbutRecord, wings: { geom: AbutWingGeom; result: AbutWingRebar }[]): void {
// 측 선택 (시안): 날개벽이 있는 측만. 고른 측이 없으면 있는 측으로.
const sideRow = el('div', undefined, 'abut-rebar-head');
sideRow.append(el('span', '측 선택', 'muted small'));
const forms = rec.wings ?? { left: 'general', right: 'general' };
for (const side of ['left', 'right'] as const) {
const has = wings.some((w) => w.geom.side === side);
const label = `${side === 'left' ? '좌 (① 쪽)' : '우 (⑤ 쪽)'} · ${WING_FORM_TEXT[forms[side]]}`;
const b = el('button', label, `abut-optt${state.abutWingSide === side ? ' on' : ''}`);
b.type = 'button';
b.disabled = !has;
b.setAttribute('aria-pressed', String(state.abutWingSide === side));
b.addEventListener('click', () => {
state.abutWingSide = side;
renderAll();
});
sideRow.append(b);
}
const cur = wings.find((w) => w.geom.side === state.abutWingSide) ?? wings[0];
if (!cur) {
box.append(sideRow, el('p', '날개벽이 없습니다 (② 날개벽 형식이 양쪽 모두 "없음"). 벽체 끝면은 W8 · W11, 흉벽 끝면은 P8 · P9 · P11로 마감합니다.', 'muted'));
return;
}
const { geom: g, result: r } = cur;
const s = r.settings;
const D = ABUT_WING_REBAR_DEFAULT;
const C = ABUT_WING_COLORS;
const SZ = ABUT_WING_SIZES;
const fck = materialFor(state.material, state.fckByMember, 'abutBody').fck;
const view = state.abutWingView;
const sideText = g.side === 'left' ? '좌' : '우';
abutRebarCard<AbutWingRebarSettings>(box, {
idPrefix: `awg-${rec.name}`,
headExtra: sideRow,
// 사용자 결정 2026-11-04: 시안 두 탭에 배면도를 더해 세 탭.
views: [['front', '전면도'], ['back', '배면도'], ['corner', '헌치 · 모서리 평면']],
view,
setView: (id) => (state.abutWingView = id as 'front' | 'back' | 'corner'),
svg: (pick) => (view === 'corner' ? abutWingRebarCornerSvg(g, r, pick) : abutWingFaceSvg(g, r, view, pick)),
caption:
view === 'corner'
? `${sideText} 날개벽 · 헌치 · 모서리 평면 · WR7-1 · 7-2 · 7-3 · WR10 각 띠의 한 단, WR9 점`
: `${sideText} 날개벽 · ${view === 'front' ? '전면도 (바깥)' : '배면도 (흙 쪽)'} · 왼쪽 교대 → 오른쪽 끝 · 수평 띠 등단면 실선 / 변화단면1 파선 / 변화단면2 점선`,
memo: `${sideText} 날개벽 ${WING_FORM_TEXT[g.form]} · 길이 ${Math.round(g.L)} · 경사 ${(g.slope * 100).toFixed(2)} % · 끝 높이 ${Math.round(g.endH)} · 헌치 ${Math.round(g.haunch)}. 맨 위 2단은 상단 경사와 나란히, 교대 철근과의 간섭은 보지 않음 (사용자 결정 2026-11-04). 좌우는 같은 입력, skew로 배면 길이가 측마다 다릅니다.`,
legendHtml: `<span style="color:${C.vertical}">━ 연직 (WR1 · 2 · 11 · 12)</span> · <span style="color:${C.horizontal}">━ 수평 등단면 (WR3 · 13) · ┅ 변화단면1 (WR4 · 14) · ┈ 변화단면2 (WR5 · 15)</span> · <span style="color:${C.haunch}">━ 헌치 WR7 · WR9 · ┅ 모서리 WR10</span> · <span style="color:${C.other}">━ WR6 · ┈ WR20</span>`,
baseTitle: '기본간격 · 날개벽 (부재별 입력, 좌우 공통)',
base: [
['수평 (mm)', s.sH, D.sH, (v) => ({ sH: v })],
['연직 (mm)', s.sV, D.sV, (v) => ({ sV: v })],
],
// 사용자 결정 2026-11-04: c · e_min · WR20 배수도 보임, WR9 개수는 없앰 (@2·s_h 자동).
inputs: [
['피복 (mm)', s.c, D.c, (v) => ({ c: v })],
['첫 철근 최소 여유 (mm)', s.eMin, D.eMin, (v) => ({ eMin: v })],
['등단면 올림 허용차 δ (mm)', s.delta, D.delta, (v) => ({ delta: v })],
['WR20 배수', s.k20, D.k20, (v) => ({ k20: v })],
],
autos: [
['형식 (②에서 정함)', WING_FORM_TEXT[g.form]],
['배면 s · 전면 · 헌치 2s', `${lenText(s.sVert ?? s.sH)} · ${lenText(2 * (s.sVert ?? s.sH))}`],
['연직 기준선 (뒷굽 끝 − c)', `전면 ${lenText(r.vertical.front.base)} · 배면 ${lenText(r.vertical.back.base)}`],
['수평 단 (교대 쪽 · 끝)', `전면 ${r.levels.front.start} · ${r.levels.front.end} / 배면 ${r.levels.back.start} · ${r.levels.back.end}`],
['WR9 개수 (헌치 대각, 자동)', String(r.bars.find((b) => b.mark === 'WR9')?.count ?? 0)],
[`l_d (fck ${fck})`, `H13 ${r.ld.H13} · H16 ${r.ld.H16} · H25 ${r.ld.H25}`],
],
bars: r.bars,
roleColor: { 연직: C.vertical, 수평: C.horizontal, 헌치: C.haunch, 모서리: C.haunch, 간격재: C.other, '경사 하단': C.other },
// 묶음별 간격 수정 (사용자 결정 2026-11-04): 연직 WR1 · 2 · 11 · 12, 수평 WR3–5 · 13–15 (배면 값, 전면은 2배).
overrides: {
WR1: { key: 'sVert', base: s.sH, label: 'WR1 · 2 · 11 · 12 수평 간격 (배면)' },
WR3: { key: 'sHoriz', base: s.sV, label: 'WR3–5 · 13–15 연직 간격 (배면)' },
},
basisNote: `근거 · 교대날개벽철근.md · spec_abut.md §4.7 · §6.5 · 규격 WR1 · 2 · 6 · 9 · 11 · 12 ${SZ.WR1} / WR3–5 · 7 · 10 · 13–15 ${SZ.WR3} / WR20 ${SZ.WR20} · 연직 배치 전면 ${r.vertical.front.text} · 배면 ${r.vertical.back.text} · WR8 · WR16–19 결번`,
set: (patch) => {
try {
applyAbutRecord(applyWingRebar(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;