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:
1 parent
d15b237100
commit
bac087ce63
1 file changed
+122
-6
+122
-6
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user