feat(abut-rebar): ③ 교대 접속슬래브 카드
- 보기 탭 평면 · 단면, 입력: 기본간격 교대축 · 교대축 직각, 피복 "80 / 100", 첫 철근 최소 여유, R8 배수 (편집, 사용자 결정 2026-11-05) - 자동 값: 상면 2s · 하면 s · R8 k·s, 하면 격자, 모따기 판정 · 짧아진 철근 수, R7 l_d. 실간격 묶음 수정은 하면 행 R3 · R4 (상면은 2배) - 노출형 안내 (매립형 규칙 적용), 공통 카드의 두 숫자 입력이 "/"도 구분자로 받음 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e9d79957c8
commit
a282618d90
1 file changed
+106
-6
+106
-6
@@ -30,9 +30,12 @@ import {
|
||||
applyWallRebar,
|
||||
applyParapetRebar,
|
||||
applyWingRebar,
|
||||
applySlabRebar,
|
||||
} 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_SLAB_REBAR_DEFAULT, ABUT_SLAB_SIZES, abutSlabGeom, abutSlabRebar, type AbutSlabGeom, type AbutSlabRebar, type AbutSlabRebarSettings } from './core/abut/rebar/slab';
|
||||
import { ABUT_SLAB_COLORS, abutSlabPlanSvg as abutSlabRebarPlanSvg, abutSlabSectionSvg as abutSlabRebarSectionSvg } from './view/abut-slab-rebar-svg';
|
||||
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';
|
||||
@@ -229,6 +232,8 @@ const state = {
|
||||
abutParapetView: 'sec' as 'sec' | 'elev' | 'dw',
|
||||
/** ③ 교대 날개벽 보기 탭 (전면도 / 배면도 / 헌치 · 모서리 평면). 측은 ② 날개벽 카드와 같은 abutWingSide. */
|
||||
abutWingView: 'front' as 'front' | 'back' | 'corner',
|
||||
/** ③ 교대 접속슬래브 보기 탭 (평면 / 단면). */
|
||||
abutSlabView: 'plan' as 'plan' | 'sec',
|
||||
abutRebarNotes: false,
|
||||
/** Member card selected on ② (시안 ② 부재 목록). */
|
||||
member: 'coping' as MemberId,
|
||||
@@ -3102,7 +3107,7 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
|
||||
box.appendChild(wrap);
|
||||
}
|
||||
|
||||
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) · 날개벽 (AD7) have rules; the rest come next. */
|
||||
/** ③ 교대 부재 (시안 순서, plan_abut AD9). 확대기초 (AD3) · 벽체 (AD4) · 흉벽 (AD5) · 날개벽 (AD7) · 접속슬래브 (AD8) have rules; 받침블럭 (AD6) comes 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 },
|
||||
@@ -3110,7 +3115,7 @@ const ABUT_REBAR_MEMBERS: { id: AbutRebarMemberId; label: string; marks: string;
|
||||
{ id: 'para', label: '흉벽·브라켓', marks: 'P1–P11', ready: true },
|
||||
{ id: 'foot', label: '확대기초', marks: 'B1–B10', ready: true },
|
||||
{ id: 'wing', label: '날개벽', marks: 'WR1–WR20', ready: true },
|
||||
{ id: 'slab', label: '접속슬래브', marks: 'R1–R8', ready: false },
|
||||
{ id: 'slab', label: '접속슬래브', marks: 'R1–R8', ready: true },
|
||||
];
|
||||
|
||||
/** The 저판 rebar of an abutment (null when the abutment does not build). */
|
||||
@@ -3175,14 +3180,49 @@ function abutWingsBuilt(rec: AbutRecord, footing: AbutFootingRebar): { geom: Abu
|
||||
}
|
||||
}
|
||||
|
||||
/** ③ with an abutment selected (2026-11-04): 부재 · 교대 list, the member card, and the RB-AF / RB-AB / RB-AP / RB-AW alerts. */
|
||||
/** The 접속슬래브 rebar of an abutment, on the ② slab plan and section (null when it does not build). */
|
||||
function abutSlabBuilt(rec: AbutRecord): { geom: AbutSlabGeom; result: AbutSlabRebar } | 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 grade = slabGradePct(slab, model.wing?.slopePct ?? 0);
|
||||
const chamfer = (sEnd: number) => (wings.find((w) => Math.abs(w.sEnd - sEnd) < 1e-9)?.haunchM ?? 0) * 1000;
|
||||
const geom = abutSlabGeom(
|
||||
slab.form,
|
||||
abutPlanOutline(model, members, wings, slab).slab,
|
||||
slabSection(slab, grade, members.shape),
|
||||
{ startEl: members.parapetTopStartEl, endEl: members.parapetTopEndEl, L: model.length, bottomEl: members.footingBottomEl ?? 0 },
|
||||
[chamfer(0), chamfer(model.length)],
|
||||
slab.lengthMm,
|
||||
grade,
|
||||
);
|
||||
return { geom, result: abutSlabRebar(geom, rec.rebar?.slab, 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 / RB-AW / RB-AS 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 wings = built ? abutWingsBuilt(rec, built.result) : [];
|
||||
const slabR = abutSlabBuilt(rec);
|
||||
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) ?? [];
|
||||
(id === 'foot'
|
||||
? built?.result.issues
|
||||
: id === 'wall'
|
||||
? wall?.result.issues
|
||||
: id === 'para'
|
||||
? para?.result.issues
|
||||
: id === 'wing'
|
||||
? wings.flatMap((w) => w.result.issues)
|
||||
: id === 'slab'
|
||||
? slabR?.result.issues
|
||||
: undefined) ?? [];
|
||||
const warnOf = (id: AbutRebarMemberId) => issuesOf(id).filter((i) => i.level !== 'I').length;
|
||||
const tabs = $<HTMLUListElement>('rebar-tabs');
|
||||
tabs.replaceChildren();
|
||||
@@ -3211,7 +3251,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) || (member.id === 'para' && !para)) {
|
||||
} else if (!built || (member.id === 'wall' && !wall) || (member.id === 'para' && !para) || (member.id === 'slab' && !slabR)) {
|
||||
$('rebar-note').textContent = `${rec.name} · 교대 모델을 만들 수 없어 배근하지 않습니다.`;
|
||||
} else if (member.id === 'wall' && wall) {
|
||||
const g = wall.geom;
|
||||
@@ -3219,6 +3259,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 === 'slab' && slabR) {
|
||||
const g = slabR.geom;
|
||||
$('rebar-note').textContent = `${rec.name} · Type-1 ${g.form === 'buried' ? '매립형' : '노출형'} · 폭 ${Math.round(g.B)} × 길이 ${Math.round(g.Ls)} · 경사 ${g.gradePct.toFixed(1)} %${g.form === 'exposed' ? ' · 매립형 규칙 적용 (사용자 결정 2026-11-05)' : ''}`;
|
||||
fillAbutSlabRebarCard(box, rec, slabR);
|
||||
} 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]}`;
|
||||
@@ -3395,7 +3439,7 @@ function abutRebarCard<S>(box: HTMLDivElement, o: AbutCardSpec<S>): void {
|
||||
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);
|
||||
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();
|
||||
@@ -3542,6 +3586,62 @@ function fillAbutWallRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { ge
|
||||
});
|
||||
}
|
||||
|
||||
/** ③ 교대 접속슬래브 카드 (시안, 교대접속슬래브철근.md §9). */
|
||||
function fillAbutSlabRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutSlabGeom; result: AbutSlabRebar }): void {
|
||||
const { geom: g, result: r } = built;
|
||||
const s = r.settings;
|
||||
const D = ABUT_SLAB_REBAR_DEFAULT;
|
||||
const C = ABUT_SLAB_COLORS;
|
||||
const SZ = ABUT_SLAB_SIZES;
|
||||
const fck = materialFor(state.material, state.fckByMember, 'abutBody').fck;
|
||||
const sA = s.sMain ?? s.sA;
|
||||
const sL = s.sDist ?? s.sL;
|
||||
const view = state.abutSlabView;
|
||||
abutRebarCard<AbutSlabRebarSettings>(box, {
|
||||
idPrefix: `asr-${rec.name}`,
|
||||
views: [['plan', '평면'], ['sec', '단면']],
|
||||
view,
|
||||
setView: (id) => (state.abutSlabView = id as 'plan' | 'sec'),
|
||||
svg: (pick) => (view === 'plan' ? abutSlabRebarPlanSvg(g, r, pick) : abutSlabRebarSectionSvg(g, r, pick)),
|
||||
caption: view === 'plan' ? '평면 · 왼쪽 절반 하면 (R3 · R4 · R8) · 오른쪽 절반 상면 (R1 · R2) · 위 = 흉벽 쪽 끝 · 모따기 모서리 R7' : '단면 (길이 방향, 폭 가운데) · 왼쪽 흉벽 쪽 끝 · 상면 R1 → R2, 하면 R3 → R4, R8은 R1 · R3을 감쌈',
|
||||
memo: g.form === 'exposed' ? '노출형도 매립형 규칙을 적용합니다 (사용자 결정 2026-11-05): 상면 철근은 상면, 하면 철근은 하면을 따라갑니다. P10 다웰과의 간섭은 보지 않습니다.' : 'P10 다웰과의 간섭은 보지 않습니다 (사용자 결정 2026-11-05).',
|
||||
legendHtml: `<span style="color:${C.main}">━ 길이 방향 주철근 (R1 · R3)</span> · <span style="color:${C.dist}">━ 교대축 방향 (R2 · R4)</span> · <span style="color:${C.corner}">━ 모서리 R7</span> · <span style="color:${C.shear}">● 전단 R8</span>`,
|
||||
baseTitle: '기본간격 · 접속슬래브 (부재별 입력)',
|
||||
base: [
|
||||
['교대축 (mm)', s.sA, D.sA, (v) => ({ sA: v })],
|
||||
['교대축 직각 (mm)', s.sL, D.sL, (v) => ({ sL: v })],
|
||||
],
|
||||
// 사용자 결정 2026-11-05: R8 배수 편집.
|
||||
inputs: [
|
||||
['첫 철근 최소 여유 (mm)', s.eMin, D.eMin, (v) => ({ eMin: v })],
|
||||
['R8 배수', s.k8, D.k8, (v) => ({ k8: v })],
|
||||
],
|
||||
pair: { label: '피복 상·하면 / 측면 (mm)', value: [s.cv, s.c], def: [D.cv, D.c], sep: '/', error: '피복은 "80 / 100"처럼 두 숫자로 입력하세요.', patch: (a, b) => ({ cv: a, c: b }) },
|
||||
autos: [
|
||||
['상면 2s · 하면 s · R8 k·s', `${lenText(2 * sA)} · ${lenText(sA)} · ${lenText(s.k8 * sA)}`],
|
||||
['하면 격자', '상면 구간을 절반으로'],
|
||||
['모따기 자름 판정', `끝점 ~ 모따기선 < c · 짧아진 철근 ${r.trimmed}개`],
|
||||
[`R7 l_d (fck ${fck})`, `H13 ${r.ld}`],
|
||||
],
|
||||
bars: r.bars,
|
||||
roleColor: { 주철근: C.main, 배력철근: C.dist, 모서리: C.corner, 전단: C.shear },
|
||||
// 묶음별 간격 수정 (사용자 결정 2026-11-05): R1 · R3 (교대축), R2 · R4 (길이). 하면 행에서 하면 간격으로 입력 (상면은 2배).
|
||||
overrides: {
|
||||
R3: { key: 'sMain', base: s.sA, label: 'R1 · R3 교대축 간격 (하면)' },
|
||||
R4: { key: 'sDist', base: s.sL, label: 'R2 · R4 길이 간격 (하면)' },
|
||||
},
|
||||
basisNote: `근거 · 교대접속슬래브철근.md · spec_abut.md §4.8 · §6.7 · 규격 R1 · R3 ${SZ.R1} / R2 · R4 · R7 · R8 ${SZ.R2} · 교대축 ${r.across.text} · 길이 ${r.along.text} · R5 · R6 결번`,
|
||||
set: (patch) => {
|
||||
try {
|
||||
applyAbutRecord(applySlabRebar(rec, patch));
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
renderAll();
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const WING_FORM_TEXT: Record<string, string> = { general: '일반형', short: '단축형', none: '없음' };
|
||||
|
||||
/** ③ 교대 날개벽 카드 (시안, 교대날개벽철근.md §10): 측 선택 + 공통 카드. 입력은 좌우 공통 (사용자 결정 2026-11-04). */
|
||||
|
||||
Reference in new issue
Block a user