diff --git a/src/core/abut/visibility.ts b/src/core/abut/visibility.ts new file mode 100644 index 0000000..b96c15f --- /dev/null +++ b/src/core/abut/visibility.ts @@ -0,0 +1,16 @@ +/** + * What the 3D view draws for the selection (2026-11-03, 형상계획 ②). + * In ② only the selected object is drawn: a selected abutment hides the piers, and a selected pier hides the abutments. + * In ① (형상 생성) the abutments are always drawn, as before; the piers follow 전체 보기 in ①, not this rule. + */ + +/** True when the pier scene (and its rebar) should be built for this stage and selection. */ +export function piersShown(stage: string, abutActive: string | null): boolean { + return !(stage === 'plan' && abutActive !== null); +} + +/** True when the abutment with this name should be drawn. */ +export function abutShown(stage: string, abutActive: string | null, name: string): boolean { + if (stage !== 'plan') return true; + return name === abutActive; +} diff --git a/src/main.ts b/src/main.ts index f86d71f..fcc08b5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -10,6 +10,7 @@ import { structureOrder } from './core/abut/order'; import { ABUT_DETAIL_LABELS, abutInputRows, abutSummaryRows } from './core/abut/summary'; import { applyAbutEdit, applyBearingEdit, applyShapeEdit, ABUT_EDIT_FIELDS, SHAPE_EDIT_FIELDS, restoreAbutDefault, setWingForm } from './core/abut/edit'; import { ABUT_SHAPE_DEFAULT, resolveShape } from './core/abut/shape'; +import { abutShown, piersShown } from './core/abut/visibility'; import { abutSectionSvg } from './view/abut-section-svg'; import type { WingForm } from './core/abut/geometry'; import { buildMembers } from './core/abut/members'; @@ -444,7 +445,7 @@ function fitToModels(position: THREE.Vector3, target: THREE.Vector3): { position const box = new THREE.Box3(); const piersRoot = allPiers ? allPiers.piers : state.scene?.root; if (piersRoot) box.expandByObject(piersRoot); - for (const g of abutGroups) box.expandByObject(g); + for (const g of abutGroups) if (g.visible) box.expandByObject(g); if (box.isEmpty()) return { position, target }; const sphere = box.getBoundingSphere(new THREE.Sphere()); return fitCamera(position, target, sphere, camera.fov, camera.aspect); @@ -496,6 +497,7 @@ function setupScene(): void { const model = buildAbut(toAbutInput(rec)); 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); abutGroups.push(group); abutGroupByName.set(rec.name, group); world.add(group); @@ -523,7 +525,8 @@ function setupScene(): void { return; } const pier = activePier(); - if (pier?.model) { + // In ② an abutment selection draws the abutment alone, so the pier scene is not built (visibility.ts). + if (pier?.model && piersShown(state.stage, state.abutActive)) { rebar3d = buildRebar3D(pier.model, state.material, state.fckByMember); rebar3d.group.visible = state.stage === 'rebar'; rebar3d.setColorMode(state.rebarColor); @@ -573,6 +576,8 @@ function abutRow(rec: AbutRecord, mode: Stage = 'shape'): HTMLLIElement { state.abutActive = rec.name; state.abutEditOpen = false; setStatus(abutSummary(rec)); + // Rebuilt first: the abutment and the camera frame are in the new scene (visibility.ts in ②). + setupScene(); focusAbut(rec.name); renderAll(); }); @@ -613,6 +618,8 @@ const ABUT_SLOTS = { function renderAbutEditor(): void { const rec = state.abutments.find((a) => a.name === state.abutActive); $('pier-info').hidden = Boolean(rec); + // ② hides the pier cards (부재 목록, 코핑 카드, 속성 복사) while an abutment is selected (style.css .abut-selected). + $('view-plan').classList.toggle('abut-selected', Boolean(rec)); for (const slot of [ABUT_SLOTS.shape, ABUT_SLOTS.plan]) { const box = $(slot.root); box.hidden = !rec; @@ -1426,9 +1433,11 @@ function wireRebarPicking(): void { function setStage(stage: Stage): void { // ① opens on 전체 보기 (default). The button is gone, so 전체 보기 returns each time ① is entered. if (stage === 'shape' && state.stage !== 'shape') state.allView = true; + const previous = state.stage; state.stage = stage; // Switching 전체 보기 on or off in ① needs the scene rebuilt for the new layout. - if ((allPiers !== null) !== multiSceneOn()) setupScene(); + // Entering or leaving ② changes which objects are drawn (visibility.ts), so the scene is rebuilt then. + if ((allPiers !== null) !== multiSceneOn() || stage === 'plan' || previous === 'plan') setupScene(); if (state.scene) setRebarView(state.scene, stage === 'rebar'); $('view-shape').hidden = stage !== 'shape'; $('view-plan').hidden = stage !== 'plan'; diff --git a/src/style.css b/src/style.css index cca3f77..436221b 100644 --- a/src/style.css +++ b/src/style.css @@ -876,3 +876,9 @@ body { background: #eef1f4; font-family: 'Noto Sans KR', system-ui, sans-serif; .abut-section .abut-footing { fill: #b9c3ce; } .abut-section .abut-wall { fill: #d8dee6; } .abut-section .abut-parapet { fill: #9aa7b5; } + +/* ② 형상계획: with an abutment selected, only the abutment card shows; the pier cards are hidden (2026-11-03). */ +#view-plan.abut-selected .members-title, +#view-plan.abut-selected #members, +#view-plan.abut-selected section[aria-labelledby="member-title"], +#view-plan.abut-selected #copy-panel { display: none; } diff --git a/tests/abut-visibility.test.ts b/tests/abut-visibility.test.ts new file mode 100644 index 0000000..14b66ef --- /dev/null +++ b/tests/abut-visibility.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from 'vitest'; +import { abutShown, piersShown } from '../src/core/abut/visibility'; + +describe('형상계획 ②: 선택한 객체만 3D에 그림', () => { + it('a selected abutment hides the piers in ②', () => { + expect(piersShown('plan', 'A1')).toBe(false); + expect(abutShown('plan', 'A1', 'A1')).toBe(true); + expect(abutShown('plan', 'A1', 'A2')).toBe(false); + }); + + it('a selected pier (no abutment selected) hides every abutment in ②', () => { + expect(piersShown('plan', null)).toBe(true); + expect(abutShown('plan', null, 'A1')).toBe(false); + expect(abutShown('plan', null, 'A2')).toBe(false); + }); + + it('① keeps the piers and every abutment as before', () => { + expect(piersShown('shape', 'A1')).toBe(true); + expect(piersShown('shape', null)).toBe(true); + expect(abutShown('shape', 'A1', 'A2')).toBe(true); + expect(abutShown('shape', null, 'A1')).toBe(true); + }); + + it('③ and ④ are not affected by the abutment selection', () => { + expect(piersShown('rebar', 'A1')).toBe(true); + expect(abutShown('rebar', null, 'A1')).toBe(true); + }); +});