feat(parts): 부재 표시 항목을 교각·교대 선택과 연동

- 부재 표시 토글은 화면에 그려진 대상에만 적용. 선택한 교각에는 교각 부재, 교대에는 교대 부재가 살아 있고, 나머지 항목은 비활성 (선택 대상만)
- 교대 항목 추가: 벽체·흉벽, 날개벽. 받침·받침블럭, 확대기초, 말뚝, 지반면, 접속슬래브는 교각과 교대에 함께 적용
- 값은 살아 있는 대상에서 읽고, 요약 개수는 활성 토글만 셈
- 테스트 635건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 16:18:11 +09:00
1 parent fbc315fdba
commit 67bebe5ec2
1 file changed
+61 -38
+61 -38
View File
@@ -85,15 +85,19 @@ interface Pier extends PierValues {
/** The four stages: ① 형상 생성, ② 형상계획, ③ 철근배근, ④ 재료표 (결과 화면). */
type Stage = 'shape' | 'plan' | 'rebar' | 'quantity';
const PART_GROUPS: { label: string; parts: PartName[] }[] = [
{ label: '받침·받침블럭', parts: ['blocks', 'pads', 'bearings'] },
{ label: '솔플레이트', parts: ['solplates'] },
{ label: '코핑', parts: ['coping'] },
{ label: '기둥', parts: ['columns'] },
{ label: '확대기초', parts: ['footings'] },
{ label: '말뚝', parts: ['piles'] },
{ label: '버림', parts: ['blinding'] },
{ label: '지반면', parts: ['ground'] },
/** 부재 표시 toggles (2026-11-03). `pier` parts follow the pier scenes; `abut` names follow the abutment groups (abut-scene.ts). */
const PART_GROUPS: { label: string; pier: PartName[]; abut: string[] }[] = [
{ label: '받침·받침블럭', pier: ['blocks', 'pads', 'bearings'], abut: ['blocks'] },
{ label: '솔플레이트', pier: ['solplates'], abut: [] },
{ label: '코핑', pier: ['coping'], abut: [] },
{ label: '기둥', pier: ['columns'], abut: [] },
{ label: '확대기초', pier: ['footings'], abut: ['footing'] },
{ label: '말뚝', pier: ['piles'], abut: ['piles'] },
{ label: '버림', pier: ['blinding'], abut: [] },
{ label: '지반면', pier: ['ground'], abut: ['ground'] },
{ label: '벽체·흉벽', pier: [], abut: ['wall', 'parapet', 'haunches'] },
{ label: '날개벽', pier: [], abut: ['wings'] },
{ label: '접속슬래브', pier: [], abut: ['slab'] },
];
const VIEWS: { kind: ViewKind; label: string }[] = [
@@ -138,9 +142,9 @@ const state = {
abutDefaults: [] as AbutRecord[],
active: null as string | null,
view: 'iso' as ViewKind,
visible: Object.fromEntries(PART_GROUPS.flatMap((g) => g.parts).map((p) => [p, true])) as Record<PartName, boolean>,
/** 접속슬래브 toggle (abutments only). */
abutSlabVisible: true,
visible: Object.fromEntries(PART_GROUPS.flatMap((g) => g.pier).map((p) => [p, true])) as Record<PartName, boolean>,
/** Abutment part visibility by abut-scene.ts group name (2026-11-03). */
abutVisible: Object.fromEntries(PART_GROUPS.flatMap((g) => g.abut).map((n) => [n, true])) as Record<string, boolean>,
scene: null as PierScene | null,
project: { name: '', bridge: '', client: '', author: '', crs: CRS, memo: '' } as ProjectInfo,
/** Which stage screen is shown: ① 형상 생성 or ② 형상계획 (plan.md C1). */
@@ -343,6 +347,18 @@ function activePier(): Pier | undefined {
}
// --- parts and views -------------------------------------------------------------
/** Pier drawn in this view: all piers in 전체 보기, else the 기준 pier unless an abutment is selected in ②. */
function pierDrawn(): boolean {
return Boolean(allPiers) || Boolean(activePier()?.model && piersShown(state.stage, state.abutActive));
}
/** Any abutment group drawn in this view. */
function abutDrawn(): boolean {
return abutGroups.some((g) => g.visible);
}
const partToggles: { group: (typeof PART_GROUPS)[number]; input: HTMLInputElement }[] = [];
function buildPartToggles(): void {
const box = $<HTMLDivElement>('parts');
const row = el('div', undefined, 'parts-row');
@@ -350,34 +366,43 @@ function buildPartToggles(): void {
const label = el('label');
const input = el('input');
input.type = 'checkbox';
input.checked = group.parts.every((p) => state.visible[p]);
input.addEventListener('change', () => {
for (const p of group.parts) {
for (const p of group.pier) {
state.visible[p] = input.checked;
setPartVisible(p, input.checked);
}
for (const n of group.abut) {
state.abutVisible[n] = input.checked;
setAbutNameVisible(n, input.checked);
}
});
partToggles.push({ group, input });
label.append(input, document.createTextNode(group.label));
row.appendChild(label);
}
// 접속슬래브 exists only on the abutments (2026-11-03): it has no pier part, so it is kept apart from state.visible.
const slabLabel = el('label');
const slabInput = el('input');
slabInput.type = 'checkbox';
slabInput.checked = state.abutSlabVisible;
slabInput.addEventListener('change', () => {
state.abutSlabVisible = slabInput.checked;
setAbutPartVisible('slab', slabInput.checked);
});
slabLabel.append(slabInput, document.createTextNode('접속슬래브'));
row.appendChild(slabLabel);
box.appendChild(row);
updatePartToggles();
}
/** Shows or hides one named part of every abutment (the 받침블럭 and 접속슬래브 of abut-scene.ts). */
function setAbutPartVisible(part: 'blocks' | 'slab', on: boolean): void {
/** A toggle is live when an object it names is drawn; its value comes from that object (2026-11-03: 선택 대상만). */
function updatePartToggles(): void {
const pier = pierDrawn();
const abut = abutDrawn();
for (const { group, input } of partToggles) {
const pierLive = pier && group.pier.length > 0;
const abutLive = abut && group.abut.length > 0;
input.disabled = !(pierLive || abutLive);
input.checked = pierLive
? group.pier.every((p) => state.visible[p])
: abutLive && group.abut.every((n) => state.abutVisible[n]);
}
updatePartsCount();
}
/** Shows or hides one named part of every abutment (the names of abut-scene.ts). */
function setAbutNameVisible(name: string, on: boolean): void {
for (const g of abutGroups) {
const member = g.getObjectByName(part);
const member = g.getObjectByName(name);
if (member) member.visible = on;
}
}
@@ -406,8 +431,6 @@ function setPartVisible(part: PartName, on: boolean): void {
else for (const s of allPiers.scenes.values()) s.parts.ground.visible = on;
return;
}
// 받침·받침블럭 also covers the abutments' blocks and pads (2026-11-03).
if (part === 'blocks') setAbutPartVisible('blocks', on);
for (const s of partScenes()) s.parts[part].visible = on;
}
@@ -524,11 +547,11 @@ function setupScene(): void {
const members = buildMembers(model, { bottomEl: rec.foundationBottomEl, pileLengthMm: rec.pileLengthMm }, rec.shape);
const group = buildAbutGroup(model, members, buildWings(model, members, rec.wings), ref.pivot, rec.groundEl, groundModel?.terrainGrid);
group.visible = abutShown(state.stage, state.abutActive, rec.name);
// The 부재 표시 toggles hold for a new abutment group too (blocks: the 받침·받침블럭 toggle; slab: 접속슬래브).
const blocks = group.getObjectByName('blocks');
if (blocks) blocks.visible = state.visible.blocks;
const slab = group.getObjectByName('slab');
if (slab) slab.visible = state.abutSlabVisible;
// The 부재 표시 toggles hold for a new abutment group too.
for (const [name, on] of Object.entries(state.abutVisible)) {
const member = group.getObjectByName(name);
if (member) member.visible = on;
}
abutGroups.push(group);
abutGroupByName.set(rec.name, group);
world.add(group);
@@ -1036,7 +1059,7 @@ function renderDefaults(): void {
let lastScrolledPier: string | null = null;
function renderAll(): void {
updatePartsCount();
updatePartToggles();
const allToggle = $<HTMLButtonElement>('all-toggle');
allToggle.setAttribute('aria-pressed', String(state.allView));
allToggle.title = `${shownPiers().filter((p) => p.model).length}기를 한 화면에 (① 형상 생성)`;
@@ -3590,8 +3613,8 @@ function setMenuOpen(which: 'views' | 'origin', open: boolean): void {
/** Counts the bar parts shown on the 3D view in the 부재 표시 summary. */
function updatePartsCount(): void {
const boxes = document.querySelectorAll<HTMLInputElement>('#parts input[type="checkbox"]');
const on = Array.from(boxes).filter((b) => b.checked).length;
const boxes = Array.from(document.querySelectorAll<HTMLInputElement>('#parts input[type="checkbox"]:not(:disabled)'));
const on = boxes.filter((b) => b.checked).length;
$('parts-count').textContent = `${on}/${boxes.length} ▾`;
}