From 67bebe5ec2492eca1a88bc2ecc863c46653a7352 Mon Sep 17 00:00:00 2001 From: ypshim Date: Tue, 3 Nov 2026 16:18:11 +0900 Subject: [PATCH] =?UTF-8?q?feat(parts):=20=EB=B6=80=EC=9E=AC=20=ED=91=9C?= =?UTF-8?q?=EC=8B=9C=20=ED=95=AD=EB=AA=A9=EC=9D=84=20=EA=B5=90=EA=B0=81?= =?UTF-8?q?=C2=B7=EA=B5=90=EB=8C=80=20=EC=84=A0=ED=83=9D=EA=B3=BC=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 부재 표시 토글은 화면에 그려진 대상에만 적용. 선택한 교각에는 교각 부재, 교대에는 교대 부재가 살아 있고, 나머지 항목은 비활성 (선택 대상만) - 교대 항목 추가: 벽체·흉벽, 날개벽. 받침·받침블럭, 확대기초, 말뚝, 지반면, 접속슬래브는 교각과 교대에 함께 적용 - 값은 살아 있는 대상에서 읽고, 요약 개수는 활성 토글만 셈 - 테스트 635건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시 Co-Authored-By: Claude Haiku 5.5 --- src/main.ts | 99 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 38 deletions(-) diff --git a/src/main.ts b/src/main.ts index 509e0cc..a0ba70b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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, - /** 접속슬래브 toggle (abutments only). */ - abutSlabVisible: true, + visible: Object.fromEntries(PART_GROUPS.flatMap((g) => g.pier).map((p) => [p, true])) as Record, + /** 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, 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 = $('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 = $('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('#parts input[type="checkbox"]'); - const on = Array.from(boxes).filter((b) => b.checked).length; + const boxes = Array.from(document.querySelectorAll('#parts input[type="checkbox"]:not(:disabled)')); + const on = boxes.filter((b) => b.checked).length; $('parts-count').textContent = `${on}/${boxes.length} ▾`; }