feat(abut): ② 벽체·흉벽 카드를 시안대로 작성 (삽도 2개, 브라켓 폭 입력, 확대기초·말뚝 분리)
- 삽도 2개 (src/view/abut-wall-svg.ts): 교대축 직각 단면(① 구간, "300 + 500 + 1600 = 2400", 흉벽 상단·브라켓 상면·교좌면·저판 EL)과 정면·배면도(교좌면 계단, 흉벽 상단 경사, 받침, 수축줄눈). 생성본 값, 가로·세로 같은 축척 - 벽체 두께는 자동 (= 브라켓 폭 + 흉벽 두께 + 교좌면 폭). 입력은 브라켓 폭(기본 300). 예전 저장 파일의 벽체 두께는 브라켓 폭으로 바꿔 읽음 (normalizeShape) - 인접 무시 기준(기본 5 mm) 입력: 이웃 받침의 교좌면 EL 차이가 이 값보다 작으면 한 구간으로 묶음 (A1 단차 71 mm, 변화 없음) - 카드 구성: 벽체 형식(기본형만, QA-8) → 삽도 → 입력 격자(시안 순서, QA-4 표시) → 자동 값(벽체 두께, 흉벽 상단 IP3/IP1, 교대축) → 구간 표 → 확인 필요 → 시안 메모 - 저판·말뚝 입력 5개를 새 "확대기초 · 말뚝" 카드로 분리. 부재 목록 6행(시안 순서), 변경 표시도 카드별로 나눔 - 테스트 갱신·추가. 전체 655건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
796a267205
commit
55b679932e
11 files changed
+401
-64
No files matched your search
+91
-15
@@ -9,7 +9,8 @@ import { buildAbut } from './core/abut/geometry';
|
||||
import { structureOrder } from './core/abut/order';
|
||||
import { ABUT_DETAIL_LABELS, ABUT_SEGMENT_HEADER, abutInputRows, abutSegmentRows, abutSummaryRows } from './core/abut/summary';
|
||||
import { applyAbutEdit, applyBearingEdit, applyBearingRule, applyShapeEdit, ABUT_EDIT_FIELDS, BEARING_RULE_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit';
|
||||
import { ABUT_SHAPE_DEFAULT, resolveShape } from './core/abut/shape';
|
||||
import { ABUT_SHAPE_DEFAULT, normalizeShape, resolveShape, wallThicknessMm } from './core/abut/shape';
|
||||
import { abutWallElevationSvg, abutWallSectionSvg, type AbutWallDrawing } from './view/abut-wall-svg';
|
||||
import { abutShown, piersShown } from './core/abut/visibility';
|
||||
import { abutBearingPlanSvg, abutBearingSectionSvg, abutSolplateSvg, type AbutBearingDetail } from './view/abut-bearing-svg';
|
||||
import {
|
||||
@@ -718,7 +719,7 @@ const ABUT_QA_NOTES = [
|
||||
const ABUT_SLOTS = {
|
||||
shape: { root: 'abut-panel', name: 'abut-panel-name', input: 'abut-panel-input', toggle: 'abut-panel-edit-toggle', editor: 'abut-panel-editor', summary: 'abut-panel-summary', basic: 'abut-panel-basic', bearings: 'abut-panel-bearings', shape: 'abut-panel-shape', msg: 'abut-panel-msg', prefix: 'abs' },
|
||||
// ② has no 편집 toggle and no basic items: the card sits under the 3D view and shows the picked member (2026-11-03).
|
||||
plan: { wing: 'abut-panel-plan-wing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
plan: { wing: 'abut-panel-plan-wing', footing: 'abut-panel-plan-footing', root: 'abut-panel-plan', name: 'abut-panel-plan-name', bearings: 'abut-panel-plan-bearings', shape: 'abut-panel-plan-shape', msg: 'abut-panel-plan-msg', prefix: 'abp' },
|
||||
} as const;
|
||||
|
||||
/** Shows the 교대 입력 in place of the pier info when an abutment is selected (2026-11-03). */
|
||||
@@ -896,6 +897,7 @@ function applyAbutMemberCard(rec: AbutRecord): void {
|
||||
const member = state.abutMember;
|
||||
$<HTMLElement>('abut-panel-plan-bearings').hidden = member !== 'bearing';
|
||||
$<HTMLElement>('abut-panel-plan-shape').hidden = member !== 'shape';
|
||||
$<HTMLElement>('abut-panel-plan-footing').hidden = member !== 'footing';
|
||||
$<HTMLElement>('abut-panel-plan-wing').hidden = member !== 'wing';
|
||||
// The card title is the picked row; its 요약 follows the name (시안 머리줄).
|
||||
const slabWidth = abutSummaryRows(rec).find(([label]) => label === '접속슬래브 폭')?.[1] ?? '—';
|
||||
@@ -1022,8 +1024,69 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
bearings.append(zLab, zIn, sLab, sIn);
|
||||
}
|
||||
|
||||
// ② 벽체·흉벽 카드 (시안 순서, 2026-11-03): 벽체 형식 → 삽도 2개 → 입력 격자 → 자동 값 → 구간 표 → 확인 필요 → 메모.
|
||||
// 저판·말뚝 입력은 확대기초 · 말뚝 카드로 나눈다. ①은 모든 형상 입력을 한 곳에 둔다.
|
||||
const footingBox = 'footing' in slot ? $<HTMLDivElement>(slot.footing) : shape;
|
||||
footingBox.replaceChildren();
|
||||
let built: { model: AbutModel; members: AbutMembers } | null = null;
|
||||
if (planCard) {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
built = { model, members: buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape) };
|
||||
} catch {
|
||||
// A model that does not build shows no 삽도 or 구간 표 (the errors are reported on import).
|
||||
}
|
||||
const kinds = el('div', undefined, 'abut-bearing-pick');
|
||||
kinds.append(el('span', '벽체 형식', 'muted small'));
|
||||
for (const [name, on] of [['기본형', true], ['브라켓형', false], ['오목형', false], ['반일체식', false]] as const) {
|
||||
const b = el('button', name, 'btn small');
|
||||
b.type = 'button';
|
||||
b.setAttribute('aria-pressed', String(on));
|
||||
if (!on) {
|
||||
b.disabled = true;
|
||||
b.title = '규칙 미정 (QA-8)';
|
||||
}
|
||||
kinds.append(b);
|
||||
}
|
||||
kinds.append(el('span', '브라켓형 · 오목형 · 반일체식은 규칙 미정 (QA-8)', 'muted small'));
|
||||
shape.append(kinds);
|
||||
if (built) {
|
||||
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
||||
const s = built.members.shape;
|
||||
const drawing: AbutWallDrawing = {
|
||||
lengthM: built.model.length,
|
||||
segments: built.members.wallSegments,
|
||||
bearings: built.model.bearings,
|
||||
footingTopEl: built.members.footingTopEl,
|
||||
footingBottomEl: built.members.footingBottomEl ?? built.members.footingTopEl - s.footingThicknessM,
|
||||
parapetTopStartEl: built.members.parapetTopStartEl,
|
||||
parapetTopEndEl: built.members.parapetTopEndEl,
|
||||
wallFrontM: s.wallFrontM,
|
||||
parapetBackM: s.parapetBackM,
|
||||
wallBackM: s.wallBackM,
|
||||
footingFrontM: s.footingFrontM,
|
||||
footingBackM: s.footingBackM,
|
||||
bracketHeightM: s.bracketHeightM,
|
||||
shrinkJointCount: v.shrinkJointCount,
|
||||
bracketWidthMm: v.bracketWidthMm,
|
||||
parapetThicknessMm: v.parapetThicknessMm,
|
||||
seatWidthMm: v.wallFrontMm,
|
||||
};
|
||||
const figs = el('div', undefined, 'abut-wall-figs');
|
||||
for (const [svg, caption] of [
|
||||
[abutWallSectionSvg(drawing), '교대축 직각 단면 · ① 구간 · 축척 같음'],
|
||||
[abutWallElevationSvg(drawing), '정면 · 배면에서 본 모습 · 왼쪽 IP3(① 쪽) → 오른쪽 IP1 · 수축줄눈 표시'],
|
||||
]) {
|
||||
const fig = el('figure', undefined, 'abut-fig');
|
||||
fig.innerHTML = svg;
|
||||
fig.append(el('figcaption', caption, 'muted small'));
|
||||
figs.append(fig);
|
||||
}
|
||||
shape.append(figs);
|
||||
}
|
||||
}
|
||||
for (const f of SHAPE_EDIT_FIELDS) {
|
||||
const current = rec.shape?.[f.key] ?? ABUT_SHAPE_DEFAULT[f.key];
|
||||
const current = normalizeShape(rec.shape)[f.key] ?? ABUT_SHAPE_DEFAULT[f.key];
|
||||
const id = idOf(`shape-${f.key}`);
|
||||
const lab = el('label', f.label);
|
||||
lab.htmlFor = id;
|
||||
@@ -1036,33 +1099,46 @@ function fillAbutSlot(slot: (typeof ABUT_SLOTS)[keyof typeof ABUT_SLOTS], rec: A
|
||||
fail(err, input, String(current));
|
||||
}
|
||||
});
|
||||
shape.append(lab, input, el('span', `기본 ${ABUT_SHAPE_DEFAULT[f.key]}`, 'muted small'));
|
||||
const note = el('span', `기본 ${ABUT_SHAPE_DEFAULT[f.key]}${f.note ? ` · ${f.note}` : ''}`, f.note ? 'muted small abut-note-warn' : 'muted small');
|
||||
(planCard && f.group === 'footing' ? footingBox : shape).append(lab, input, note);
|
||||
}
|
||||
// 구간 표 (시안 하단): one row per 교좌면 step, from the model (2026-11-03).
|
||||
if (planCard) {
|
||||
try {
|
||||
const model = buildAbut(toAbutInput(rec));
|
||||
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
|
||||
// 자동 값 (시안 입력 격자 마지막 줄): 벽체 두께, 흉벽 상단 IP3 / IP1 끝 (가정, QA-4), 교대축.
|
||||
const v = { ...ABUT_SHAPE_DEFAULT, ...normalizeShape(rec.shape) };
|
||||
shape.append(el('label', '벽체 두께 (자동)'), el('span', `${wallThicknessMm(v)} mm`, 'num'), el('span', '= 브라켓 폭 + 흉벽 두께 + 교좌면 폭', 'muted small'));
|
||||
if (built) {
|
||||
shape.append(
|
||||
el('label', '흉벽 상단 IP3 / IP1 끝 (m)'),
|
||||
el('span', `${built.members.parapetTopEndEl.toFixed(3)} / ${built.members.parapetTopStartEl.toFixed(3)}`, 'num'),
|
||||
el('span', '가정 (QA-4)', 'muted small abut-note-warn'),
|
||||
el('label', '교대축 (자동)'),
|
||||
el('span', `${built.model.length.toFixed(3)} m`, 'num'),
|
||||
el('span', 'IP1 → IP3', 'muted small'),
|
||||
);
|
||||
// 구간 표 (시안 하단): one row per 교좌면 step.
|
||||
const table = document.createElement('table');
|
||||
table.className = 'abut-seg';
|
||||
const thead = table.createTHead().insertRow();
|
||||
for (const h of ABUT_SEGMENT_HEADER) thead.append(el('th', h));
|
||||
const tbody = table.createTBody();
|
||||
for (const row of abutSegmentRows(model, members)) {
|
||||
for (const row of abutSegmentRows(built.model, built.members)) {
|
||||
const tr = tbody.insertRow();
|
||||
for (const cell of row) tr.append(el('td', cell));
|
||||
}
|
||||
shape.append(table);
|
||||
} catch {
|
||||
// A model that does not build shows no 구간 표 (the errors are reported on import).
|
||||
shape.append(el('p', '교좌면 계단 구간 · IP3(① 쪽)에서 잰 거리', 'lbl'), table);
|
||||
}
|
||||
}
|
||||
// 확인 필요 (spec_abut, 시안 오른쪽 패널): the open 교대 questions that touch the shape (2026-11-03).
|
||||
if (planCard) {
|
||||
// 확인 필요 (spec_abut, 시안 오른쪽 패널): the open 교대 questions that touch the shape (2026-11-03).
|
||||
const qa = el('div', undefined, 'abut-qa');
|
||||
qa.append(el('p', '확인 필요 (spec_abut)', 'lbl'));
|
||||
for (const line of ABUT_QA_NOTES) qa.append(el('p', line, 'muted small'));
|
||||
shape.append(qa);
|
||||
shape.append(
|
||||
el(
|
||||
'p',
|
||||
'시안 메모 · 벽체는 교좌면 계단 구간마다 높이가 다릅니다. 흉벽 높이 2500은 ① 구간 교좌면 기준(가정, QA-4)이고, Level 좌·우는 계산에 쓰지 않습니다. 인접 무시 기준보다 작은 교좌면 단차는 한 구간으로 묶습니다.',
|
||||
'muted small abut-memo',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user