feat(plan): 형상계획 ②에서 선택한 교대만 3D와 카드에 표시

- 교대를 고르면 교각 장면·철근을 만들지 않고 선택한 교대만 그림. 교각을 고르면 교대를 숨김 (src/core/abut/visibility.ts)
- ①의 전체 보기와 ③·④는 기존 동작 유지
- 교대 선택 시 ②의 부재 목록·코핑 카드·속성 복사를 숨기고 교대 카드만 표시
- 카메라 맞춤은 보이는 객체만 포함
- 테스트 623건 통과, 타입 검사·빌드 통과. 브라우저 시각 확인은 미실시

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
ypshimandClaude Haiku 5.5 committed 2026-11-03 15:06:50 +09:00
1 parent 5af88a511e
commit 929f8eb4a2
4 files changed
+62 -3

No files matched your search

+16
View File
@@ -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;
}
+12 -3
View File
@@ -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);
$<HTMLElement>('pier-info').hidden = Boolean(rec);
// ② hides the pier cards (부재 목록, 코핑 카드, 속성 복사) while an abutment is selected (style.css .abut-selected).
$<HTMLElement>('view-plan').classList.toggle('abut-selected', Boolean(rec));
for (const slot of [ABUT_SLOTS.shape, ABUT_SLOTS.plan]) {
const box = $<HTMLElement>(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');
$<HTMLElement>('view-shape').hidden = stage !== 'shape';
$<HTMLElement>('view-plan').hidden = stage !== 'plan';
+6
View File
@@ -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; }
+28
View File
@@ -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);
});
});