feat(abut-rebar): ③ 교대 화면 틀과 확대기초 배근 카드
- ③에서 교대를 고르면 부재 · 교대 목록(받침블럭 → 벽체 → 흉벽·브라켓 → 확대기초 → 날개벽 → 접속슬래브)과 카드를 보여 줌. 확대기초만 규칙이 있고 나머지는 '다음 단계'. 선택한 교대만 3D에 그림 - 확대기초 카드: 보기 탭 3개(교대축 직각 단면 · 교대축 단면 · 상면 평면)와 번호 풍선 강조, 범례, 기본간격 · 배근 방향 ① / ② · 피복 · 여유 · B9 배수 입력, 자동 값(cycle 간격 · B10 위 여유 · l_s · 옆붙임), 철근표(묶음별 실간격 수정 · 수정됨 · 되돌리기, 비고 펼치기), 근거 줄, RB-AF 알림 - 입력은 AbutRecord.rebar.footing에 바꾼 값만 저장 · 프로젝트 파일 검증. fck는 공통 설정의 기초부 값 Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e00e43ca61
commit
b06bf368b6
9 files changed
+591
-10
No files matched your search
+321
-7
@@ -26,7 +26,12 @@ import {
|
||||
setSlabForm,
|
||||
SLAB_EDIT_FIELDS,
|
||||
setAbutGround,
|
||||
applyFootingRebar as applyAbutFootingRebar,
|
||||
} 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 { skewAngle } from './core/abut/rebar/skew-layout';
|
||||
import { lapLength } from './core/abut/rebar/splice';
|
||||
import { ABUT_FOOTING_COLORS, abutFootingRebarPlanSvg, abutFootingSectionLSvg, abutFootingSectionWSvg } from './view/abut-footing-rebar-svg';
|
||||
import { abutGroundIssues, supportTopEl, type AbutGround } from './core/abut/ground';
|
||||
import { ABUT_SLAB_DEFAULT, resolveSlab, SLAB_FORM_LABEL, slabGradePct, slabSection, slabSlopeLabel, type SlabForm } from './core/abut/slab';
|
||||
import { abutSlabPlanSvg, abutSlabSectionSvg } from './view/abut-slab-svg';
|
||||
@@ -65,7 +70,7 @@ import { pileGeometry } from './core/geometry';
|
||||
import { rebarSummary, type RebarRow } from './core/rebar/summary';
|
||||
import { shearSegments } from './core/rebar/coping';
|
||||
import { buildRebar3D, type RebarColorMode, type RebarScene } from './view/rebar3d';
|
||||
import { DEFAULT_MATERIAL, FCK_MEMBERS, fckChanged, fyOf, GRADE_CHOICES, IMPORTED_FCK, isAssumedMaterial, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common';
|
||||
import { DEFAULT_MATERIAL, FCK_MEMBERS, fckChanged, fyOf, GRADE_CHOICES, IMPORTED_FCK, isAssumedMaterial, materialFor, materialFrom, type BendFactors, type FckByMember, type FckMember, type GradeKey, type Material } from './core/rebar/common';
|
||||
import { buildQuantity, quantityRows, type Quantity, type QuantityLine } from './core/rebar/quantity';
|
||||
import { groundElFor, parseGroundModel, strataRowsFor, type GroundMapLayer, type GroundModelFile } from './core/ground-model';
|
||||
import { CORROSION_ALLOWANCE_MM, GROUND_POSITIONS, parseNValue, SEA_DATUM, STRATA_NAMES } from './core/ground';
|
||||
@@ -205,6 +210,11 @@ const state = {
|
||||
rebarColor: 'role' as RebarColorMode,
|
||||
/** Member tab on ③ (철근표). */
|
||||
rebarMember: 'coping' as RebarRow['member'],
|
||||
/** ③ 교대 (2026-11-04): 부재, 강조한 철근 번호, 보기 탭, 비고 펼치기. */
|
||||
abutRebarMember: 'foot' as AbutRebarMemberId,
|
||||
abutRebarPick: null as string | null,
|
||||
abutRebarView: 'w' as 'w' | 'l' | 'plan',
|
||||
abutRebarNotes: false,
|
||||
/** Member card selected on ② (시안 ② 부재 목록). */
|
||||
member: 'coping' as MemberId,
|
||||
/** True once the project has a file on disk (first save via 시안 ⑤, or an opened file). Later saves go straight to it. */
|
||||
@@ -1563,6 +1573,13 @@ function applyAbutMemberCard(rec: AbutRecord): void {
|
||||
$<HTMLElement>('abut-panel-plan-msg').textContent = ABUT_MEMBER_EDITABLE[member] ? '' : `${rec.name}: 이 부재의 입력은 2단계에서 추가합니다. 현재 값만 보여 줍니다.`;
|
||||
}
|
||||
|
||||
/** Replaces one abutment record, rebuilds the 3D view and redraws (② cards and ③ 교대 배근). */
|
||||
function applyAbutRecord(next: AbutRecord): void {
|
||||
state.abutments = state.abutments.map((a) => (a.name === next.name ? next : a));
|
||||
setupScene();
|
||||
renderAll();
|
||||
}
|
||||
|
||||
/** Fills one 교대 입력 panel: the basic items, the 받침 values and the 형상 values. Each change is checked and applied. */
|
||||
function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: AbutRecord): void {
|
||||
$<HTMLElement>(slot.name).textContent = rec.name;
|
||||
@@ -1600,11 +1617,7 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
bearings.replaceChildren();
|
||||
shape.replaceChildren();
|
||||
msg.textContent = '';
|
||||
const apply = (next: AbutRecord) => {
|
||||
state.abutments = state.abutments.map((a) => (a.name === next.name ? next : a));
|
||||
setupScene();
|
||||
renderAll();
|
||||
};
|
||||
const apply = (next: AbutRecord) => applyAbutRecord(next);
|
||||
const fail = (err: unknown, input: HTMLInputElement, back: string) => {
|
||||
msg.textContent = (err as Error).message;
|
||||
input.value = back;
|
||||
@@ -1860,7 +1873,7 @@ function renderPierRows(list: HTMLUListElement, mode: Stage): void {
|
||||
list.replaceChildren();
|
||||
const visible = state.piers.filter((p) => state.filter === 'all' || p.input.type === state.filter);
|
||||
// 교대 rows (2026-11-03, AB): A1 first, A2 last, shown in ① with the piers when the filter is 전체.
|
||||
const abuts = (mode === 'shape' || mode === 'plan') && state.filter === 'all' ? structureOrder(state.abutments, visible.map((p) => p.input.name)) : [];
|
||||
const abuts = (mode === 'shape' || mode === 'plan' || mode === 'rebar') && state.filter === 'all' ? structureOrder(state.abutments, visible.map((p) => p.input.name)) : [];
|
||||
const abutRefs = abuts.filter((r) => r.kind === 'abut');
|
||||
for (const ref of abutRefs.filter((r) => r.type === 'A1')) {
|
||||
const rec = state.abutments.find((a) => a.name === ref.name);
|
||||
@@ -3053,10 +3066,311 @@ function renderCopingDrawings(box: HTMLDivElement, pier: Pier): void {
|
||||
box.appendChild(wrap);
|
||||
}
|
||||
|
||||
/** ③ 교대 부재 (시안 순서, plan_abut AD9). Only the 확대기초 has rules today (AD3); 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: false },
|
||||
{ id: 'para', label: '흉벽·브라켓', marks: 'P1–P11', ready: false },
|
||||
{ id: 'foot', label: '확대기초', marks: 'B1–B10', ready: true },
|
||||
{ id: 'wing', label: '날개벽', marks: 'WR1–WR20', ready: false },
|
||||
{ id: 'slab', label: '접속슬래브', marks: 'R1–R8', ready: false },
|
||||
];
|
||||
|
||||
/** The 저판 rebar of an abutment (null when the abutment does not build). */
|
||||
function abutFootingBuilt(rec: AbutRecord): { geom: AbutFootingGeom; result: AbutFootingRebar; skewK: number } | null {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
const geom = abutFootingGeom(model.length, members.shape, skewAngle(rec.skewDeg));
|
||||
const result = abutFootingRebar(geom, rec.rebar?.footing, materialFor(state.material, state.fckByMember, 'footing'), rec.name);
|
||||
return { geom, result, skewK: abutSkewK(model) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** ③ with an abutment selected (2026-11-04): 부재 · 교대 list, the member card, and the RB-AF alerts. */
|
||||
function renderAbutRebar(rec: AbutRecord): void {
|
||||
const built = abutFootingBuilt(rec);
|
||||
const warn = built ? built.result.issues.filter((i) => i.level !== 'I').length : 0;
|
||||
const tabs = $<HTMLUListElement>('rebar-tabs');
|
||||
tabs.replaceChildren();
|
||||
for (const m of ABUT_REBAR_MEMBERS) {
|
||||
const li = el('li');
|
||||
const btn = el('button', undefined, 'member');
|
||||
btn.type = 'button';
|
||||
btn.setAttribute('aria-pressed', String(m.id === state.abutRebarMember));
|
||||
btn.append(el('span', m.label), el('span', ` Type-1 · ${m.marks}`, 'muted small'));
|
||||
if (!m.ready) btn.append(el('span', '다음 단계', 'badge'));
|
||||
else if (warn > 0) btn.append(el('span', String(warn), 'badge warn'));
|
||||
btn.addEventListener('click', () => {
|
||||
state.abutRebarMember = m.id;
|
||||
state.abutRebarPick = null;
|
||||
renderAll();
|
||||
});
|
||||
li.appendChild(btn);
|
||||
tabs.appendChild(li);
|
||||
}
|
||||
const box = $<HTMLDivElement>('rebar-table');
|
||||
box.replaceChildren();
|
||||
const issues = $<HTMLUListElement>('rebar-issues');
|
||||
issues.replaceChildren();
|
||||
const member = ABUT_REBAR_MEMBERS.find((m) => m.id === state.abutRebarMember)!;
|
||||
$('rebar-title').textContent = `${member.label} 배근`;
|
||||
if (!member.ready) {
|
||||
$('rebar-note').textContent = `${rec.name} · ${member.label} (${member.marks})`;
|
||||
box.append(el('p', `${member.label} 배근은 다음 단계에서 추가합니다 (철근 기획서 확정, plan_abut AD4–AD8).`, 'muted'));
|
||||
} else if (!built) {
|
||||
$('rebar-note').textContent = `${rec.name} · 교대 모델을 만들 수 없어 배근하지 않습니다.`;
|
||||
} else {
|
||||
const g = built.geom;
|
||||
const toe = Math.round(g.W - g.xf);
|
||||
$('rebar-note').textContent = `${rec.name} · Type-1 · ${Math.round(g.L)} × ${Math.round(g.W)} × ${Math.round(g.H)} · ${g.plan === 'skew' ? 'skew 평행사변형' : '직사각'} · 앞굽 ${toe} / 벽체 ${Math.round(g.xf - g.xb)} / 뒷굽 ${Math.round(g.xb)}`;
|
||||
fillAbutFootingRebarCard(box, rec, built);
|
||||
}
|
||||
for (const i of built?.result.issues ?? []) {
|
||||
const li = el('li', `${i.code} ${i.message}`);
|
||||
if (i.level === 'W' || i.level === 'E') li.classList.add('warn');
|
||||
issues.appendChild(li);
|
||||
}
|
||||
if (issues.childElementCount === 0) issues.appendChild(el('li', '알림이 없습니다.'));
|
||||
$('step3-status').textContent = `${rec.name} 확대기초 계산됨 · 경고 ${warn}`;
|
||||
}
|
||||
|
||||
/** ③ 교대 확대기초 카드 (시안, 교대확대기초철근.md §10): 보기 탭 · 삽도 → 범례 → 기본간격 · 배근 방향 · 입력 → 자동 값 → 철근표 → 근거. */
|
||||
function fillAbutFootingRebarCard(box: HTMLDivElement, rec: AbutRecord, built: { geom: AbutFootingGeom; result: AbutFootingRebar; skewK: number }): void {
|
||||
const { geom, result: r, skewK } = built;
|
||||
const s = r.settings;
|
||||
const pick = state.abutRebarPick;
|
||||
const set = (patch: Partial<AbutFootingRebarSettings>) => {
|
||||
try {
|
||||
applyAbutRecord(applyAbutFootingRebar(rec, patch));
|
||||
} catch (err) {
|
||||
setError((err as Error).message);
|
||||
renderAll();
|
||||
}
|
||||
};
|
||||
const togglePick = (mark: string) => {
|
||||
state.abutRebarPick = state.abutRebarPick === mark ? null : mark;
|
||||
renderAll();
|
||||
};
|
||||
// 보기 탭 + 전체 보기 + 강조 상태.
|
||||
const head = el('div', undefined, 'abut-rebar-head');
|
||||
const views: ['w' | 'l' | 'plan', string][] = [['w', '교대축 직각 단면 (폭 W)'], ['l', '교대축 단면 (길이 L)'], ['plan', '상면 평면']];
|
||||
for (const [id, label] of views) {
|
||||
const b = el('button', label, `abut-optt${state.abutRebarView === id ? ' on' : ''}`);
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(state.abutRebarView === id));
|
||||
b.addEventListener('click', () => {
|
||||
state.abutRebarView = id;
|
||||
renderAll();
|
||||
});
|
||||
head.append(b);
|
||||
}
|
||||
const all = el('button', '전체 보기', 'btn small');
|
||||
all.type = 'button';
|
||||
all.disabled = pick === null;
|
||||
all.addEventListener('click', () => {
|
||||
state.abutRebarPick = null;
|
||||
renderAll();
|
||||
});
|
||||
head.append(all);
|
||||
box.append(head);
|
||||
const picked = pick ? r.bars.filter((b) => b.mark === pick || (pick === 'B10' && b.mark.startsWith('B10'))) : [];
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
pick === null
|
||||
? '전체 표시 · 표의 번호나 도면의 번호 풍선을 누르면 그 철근만 강조합니다'
|
||||
: `강조 중 · ${picked.map((b) => b.mark).join(' · ')}${picked[0] ? ` — ${picked[0].position}` : ''} · 같은 번호를 다시 누르거나 [전체 보기]로 해제`,
|
||||
'muted small',
|
||||
),
|
||||
);
|
||||
const fig = el('figure', undefined, 'ground-fig abut-rebar-fig');
|
||||
fig.innerHTML =
|
||||
state.abutRebarView === 'w' ? abutFootingSectionWSvg(geom, r, pick) : state.abutRebarView === 'l' ? abutFootingSectionLSvg(geom, r, pick) : abutFootingRebarPlanSvg(geom, r, skewK, pick);
|
||||
fig.querySelectorAll<SVGGElement>('[data-pick]').forEach((g) => {
|
||||
const mark = g.getAttribute('data-pick')!;
|
||||
g.addEventListener('click', () => togglePick(mark));
|
||||
g.addEventListener('keydown', (ev) => {
|
||||
if (ev.key === 'Enter' || ev.key === ' ') {
|
||||
ev.preventDefault();
|
||||
togglePick(mark);
|
||||
}
|
||||
});
|
||||
});
|
||||
const caption =
|
||||
state.abutRebarView === 'w'
|
||||
? '교대축 직각 단면 · 폭 W (왼쪽 배면 · 뒷굽 → 오른쪽 전면 · 앞굽) · 선 = B1–B4, 점 = B5–B8 · B10'
|
||||
: state.abutRebarView === 'l'
|
||||
? `교대축 단면 · 길이 L (가로 축척 0.4배) · B1·B3 / B2·B4 같은 높이에 번갈아 (실간격 ${s.sMain ?? s.sL}) · B1은 B2 옆 +${r.sideShift.B1.toFixed(2)}`
|
||||
: `상면 평면 · 배근 방향 ${r.dir === 'skew' ? '① skew 방향 (날개벽 선과 나란히)' : '② 교대축 직각 (가운데 격자 + 끝 skew 철근 B1-S)'} · 위 = 전면, 왼쪽 IP3 → 오른쪽 IP1`;
|
||||
fig.append(el('figcaption', caption, 'muted small'));
|
||||
box.append(fig);
|
||||
const C = ABUT_FOOTING_COLORS;
|
||||
const legend = el('p', undefined, 'muted small abut-rebar-legend');
|
||||
legend.innerHTML = `<span style="color:${C.main}">━ 주철근 폭 방향 (B1–B4${r.skewEnd ? ' · B1-S·B2-S' : ''})</span> · <span style="color:${C.dist}">━ 배력 교대축 방향 (B5–B8)</span> · <span style="color:${C.shear}">━ 전단 B9</span> · <span style="color:${C.side}">┅ 측면 B10</span> · 색은 역할 기준 (교대는 폭 방향 B1–B4가 주철근)`;
|
||||
box.append(legend);
|
||||
|
||||
// 입력: 기본간격 · 배근 방향 · 피복 · 여유 · B9 배수.
|
||||
const num = (into: HTMLElement, label: string, value: number, apply: (v: number) => Partial<AbutFootingRebarSettings>, def: number) => {
|
||||
const id = `abr-${rec.name}-${label}`;
|
||||
const lab = el('label', label);
|
||||
lab.htmlFor = id;
|
||||
const input = el('input');
|
||||
input.id = id;
|
||||
input.type = 'number';
|
||||
input.step = 'any';
|
||||
input.value = String(value);
|
||||
if (value !== def) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => set(apply(Number(input.value))));
|
||||
const cell = el('div');
|
||||
cell.append(lab, input);
|
||||
into.append(cell);
|
||||
};
|
||||
const grid = el('div', undefined, 'abut-rebar-inputs');
|
||||
const D = ABUT_FOOTING_REBAR_DEFAULT;
|
||||
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);
|
||||
box.append(grid);
|
||||
box.append(
|
||||
optionTiles(
|
||||
'B1–B4 배근 방향',
|
||||
[
|
||||
{ label: '① skew 방향', on: r.dir === 'skew', isDefault: true, disabled: geom.plan === 'rect' ? '직사각 평면에서는 선택 불가' : undefined, pick: () => r.dir !== 'skew' && set({ dir: 'skew' }) },
|
||||
{ label: '② 교대축 직각', on: r.dir === 'perp', pick: () => r.dir !== 'perp' && set({ dir: 'perp' }) },
|
||||
],
|
||||
'직사각 평면이면 ② 고정 (RB-AF04)',
|
||||
),
|
||||
);
|
||||
const grid2 = el('div', undefined, 'abut-rebar-inputs');
|
||||
num(grid2, '상면·측면 피복 (mm)', s.cTop, (v) => ({ cTop: v, cSide: v }), D.cTop);
|
||||
num(grid2, '하면 피복 (mm)', s.cBottom, (v) => ({ cBottom: v }), D.cBottom);
|
||||
num(grid2, '첫 철근 최소 여유 (mm)', s.eMin, (v) => ({ eMin: v }), D.eMin);
|
||||
{
|
||||
const id = `abr-${rec.name}-k9`;
|
||||
const lab = el('label', 'B9 배수 (교대축 × 폭)');
|
||||
lab.htmlFor = id;
|
||||
const input = el('input');
|
||||
input.id = id;
|
||||
input.type = 'text';
|
||||
input.value = `${s.k9L} × ${s.k9W}`;
|
||||
if (s.k9L !== D.k9L || s.k9W !== D.k9W) input.classList.add('changed-input');
|
||||
input.addEventListener('change', () => {
|
||||
const n = input.value.split(/[×x*,\s]+/).filter(Boolean).map(Number);
|
||||
if (n.length !== 2) {
|
||||
setError('B9 배수는 "2 × 2"처럼 두 정수로 입력하세요.');
|
||||
renderAll();
|
||||
return;
|
||||
}
|
||||
set({ k9L: n[0], k9W: n[1] });
|
||||
});
|
||||
const cell = el('div');
|
||||
cell.append(lab, input);
|
||||
grid2.append(cell);
|
||||
}
|
||||
// 자동 값.
|
||||
const fck = materialFor(state.material, state.fckByMember, 'footing').fck;
|
||||
const fy = (size: string) => fyOf(state.material, size);
|
||||
const auto = (label: string, value: string) => {
|
||||
const cell = el('div', undefined, 'abut-rebar-auto');
|
||||
cell.append(el('span', label, 'muted small'), el('span', value, 'num'));
|
||||
grid2.append(cell);
|
||||
};
|
||||
auto('cycle 간격', `B1–B4 ${2 * (s.sMain ?? s.sL)} · B5–B8 ${2 * (s.sDist ?? s.sW)}`);
|
||||
auto('B10 위 여유 (계산)', `${Math.round(r.side.topGap)} (≥ ${s.eTop})`);
|
||||
auto(`이음 l_s = 1.3 l_d (fck ${fck})`, `H16 ${lapLength(15.9, fy('H16'), fck)} · H19 ${lapLength(19.1, fy('H19'), fck)}`);
|
||||
auto('B1 옆붙임 · B5 옆붙임', `+${r.sideShift.B1.toFixed(2)} · +${r.sideShift.B5.toFixed(1)}`);
|
||||
box.append(grid2);
|
||||
|
||||
// 철근표.
|
||||
const bar = el('div', undefined, 'abut-rebar-tablehead');
|
||||
bar.append(el('span', '가정값 없음 · 모든 철근이 규칙 계산', 'muted small'));
|
||||
const notes = el('button', state.abutRebarNotes ? '비고 접기' : '비고 모두 펼치기', 'btn small');
|
||||
notes.type = 'button';
|
||||
notes.addEventListener('click', () => {
|
||||
state.abutRebarNotes = !state.abutRebarNotes;
|
||||
renderAll();
|
||||
});
|
||||
bar.append(notes);
|
||||
box.append(bar);
|
||||
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.main, 보조: C.dist, '전단·간격재': C.shear, 측면: C.side };
|
||||
// 철근별 간격 수정 (RB-AF10): the first row of each linked group takes the 실간격; B9 goes by the 배수 above.
|
||||
const overrideOf: Record<string, { key: 'sMain' | 'sDist' | 'sSide'; base: number } | undefined> = {
|
||||
B1: { key: 'sMain', base: s.sL },
|
||||
B5: { key: 'sDist', base: s.sW },
|
||||
'B10-1': { key: 'sSide', base: s.sV },
|
||||
};
|
||||
for (const b of r.bars) {
|
||||
const on = pick !== null && (b.mark === pick || (pick === 'B10' && b.mark.startsWith('B10')));
|
||||
const tr = body.insertRow();
|
||||
if (on) tr.classList.add('hl');
|
||||
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}`;
|
||||
const markCell = el('td', b.mark, 'bar-no');
|
||||
markCell.style.cursor = 'pointer';
|
||||
markCell.addEventListener('click', () => togglePick(b.mark.startsWith('B10') ? 'B10' : b.mark));
|
||||
const actual = el('td');
|
||||
const ov = overrideOf[b.mark];
|
||||
if (ov) {
|
||||
const input = el('input');
|
||||
input.type = 'number';
|
||||
input.step = 'any';
|
||||
input.value = b.actual;
|
||||
input.style.width = '70px';
|
||||
input.setAttribute('aria-label', `${b.mark} 묶음 실간격`);
|
||||
input.addEventListener('change', () => set({ [ov.key]: Number(input.value) === ov.base ? undefined : Number(input.value) }));
|
||||
actual.append(input);
|
||||
if (b.modified) {
|
||||
actual.append(el('span', ' 수정됨', 'muted small abut-note-warn'));
|
||||
const back = el('button', '되돌리기', 'btn small');
|
||||
back.type = 'button';
|
||||
back.addEventListener('click', () => set({ [ov.key]: undefined }));
|
||||
actual.append(back);
|
||||
}
|
||||
} else {
|
||||
actual.textContent = b.actual + (b.modified && b.mark !== 'B9' ? ' · 수정됨' : '');
|
||||
}
|
||||
tr.append(role, markCell, el('td', b.size), el('td', b.spacing), actual, el('td', b.shape), el('td', String(b.length)), el('td', String(b.count)), el('td', b.position));
|
||||
if (state.abutRebarNotes || on) {
|
||||
const nr = body.insertRow();
|
||||
const cell = el('td', `${b.mark} 비고 · ${b.note}`, 'muted small');
|
||||
cell.colSpan = 9;
|
||||
nr.append(cell);
|
||||
}
|
||||
}
|
||||
const scroll = el('div', undefined, 'table-scroll');
|
||||
scroll.append(table);
|
||||
box.append(scroll);
|
||||
const sizes = ABUT_FOOTING_SIZES;
|
||||
box.append(
|
||||
el(
|
||||
'p',
|
||||
`근거 · 교대확대기초철근.md · spec_abut.md §6.2 · 규격 B1·B3 ${sizes.top} / B2·B4 ${sizes.bottom} / B5–B8 ${sizes.dist} / B9 ${sizes.shear} / B10 ${sizes.side} · 하면 ${r.dist.bottomText} · 상면 ${r.dist.topText} · 측면 ${r.side.text} · 교대축 ${r.main.text}`,
|
||||
'muted small',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function renderRebar(): void {
|
||||
const pier = activePier();
|
||||
// 교각 목록 with the 배근 status (시안 ③). The checks are the same as in ① and ②.
|
||||
renderPierRows($<HTMLUListElement>('rebar-pier-items'), 'rebar');
|
||||
// ③ 교대 (2026-11-04): a selected abutment shows its own member list and card.
|
||||
const abutRec = state.abutments.find((a) => a.name === state.abutActive);
|
||||
if (abutRec) {
|
||||
renderAbutRebar(abutRec);
|
||||
renderMaterial();
|
||||
return;
|
||||
}
|
||||
$('rebar-title').textContent = '철근표';
|
||||
const tabs = $<HTMLUListElement>('rebar-tabs');
|
||||
tabs.replaceChildren();
|
||||
for (const m of REBAR_MEMBERS) {
|
||||
|
||||
Reference in new issue
Block a user