-
-
3D 구조물 목록
+
+
3D 구조물 선택
부재 클릭 시 강조
-
+
diff --git a/static/css/style.css b/static/css/style.css
index 07f69fc..a23fbff 100644
--- a/static/css/style.css
+++ b/static/css/style.css
@@ -617,12 +617,15 @@ header {
gap: 8px;
}
+/* 1.2: 2단계 3D 구조물 선택 영역 날개벽 등 텍스트 크기 1.5배 확대 */
.struct-card-step2-title {
- font-size: 12px;
+ font-size: 17.5px !important;
font-weight: 700;
display: flex;
align-items: center;
- gap: 6px;
+ gap: 8px;
+ color: var(--text-primary);
+ letter-spacing: -0.3px;
}
.struct-card-step2-tag {
diff --git a/static/js/app.js b/static/js/app.js
index faf6180..406546b 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -453,9 +453,10 @@ class WingwallApp {
await this.recalculateModel();
this.viewer3d.setStepMode(2);
- this.viewer3d.fitView();
+ this.viewer3d.fitView(0.80);
this.renderStep2Sidebar();
- this.selectObject(this.selectedObject || 'WW_END_LEFT');
+ // Requirement 2.2.1: 2단계 진입 시 기본 선택 구조물은 ‘암거본체’로 설정
+ this.selectObject('CULVERT');
} else if (stepNum === 3) {
// Step 3 is exclusively 3D rebar model view (세로 2단: 좌측 구조물 선택 + 우측 3D 뷰어 & HUD)
this.setViewMode('3d');
@@ -513,29 +514,12 @@ class WingwallApp {
const el = document.getElementById('overlay-3d-legend');
if (!el) return;
- const lLen = this.modelData?.objects?.ww_left?.dimensions?.length ?? this.params.left.length;
- const rLen = this.modelData?.objects?.ww_right?.dimensions?.length ?? this.params.right.length;
-
- if (this.currentStep === 3) {
- // Requirement 3.1.1 & 3.1.2: 3단계에서 불필요한 안내 문구 및 텍스트 완전 삭제
+ if (this.currentStep === 2 || this.currentStep === 3) {
+ // Requirement 2.1 & 3.1: 2단계 및 3단계에서 불필요한 안내 문구 및 텍스트 완전 삭제
el.innerHTML = '';
return;
} else {
- const genDwgs = this.getGeneralDrawings();
- const genName = genDwgs.length > 0 ? genDwgs.map(d => d.fileName).join(', ') : '선택된 일반도 없음';
- el.innerHTML = `
-
- [2단계: 일반도 전용 외형 모델] ${genName}
-
-
- ■ 좌측 날개벽(${Number(lLen).toFixed(2)}m)
- ■ 우측 날개벽(${Number(rLen).toFixed(2)}m)
- ■ 암거 본체 (배근도 제외)
-
-
- 좌클릭: 회전 | 우클릭: 이동 | 휠: 줌 | 부재 클릭: 외형 제원 확인 및 3D 강조
-
- `;
+ el.innerHTML = '';
}
}
@@ -555,9 +539,9 @@ class WingwallApp {
targetDrawings = this.drawings;
}
} else if (this.currentStep === 2) {
- const gen = this.getGeneralDrawings();
- stepBadgeHtml = `
2단계: 일반도 전용 외형 모델 (${gen.length}종)`;
- targetDrawings = gen;
+ // Requirement 2.1.1: 메인창 상단에 표시되는 불러온 도면명과 ‘[2단계: 일반도 전용 외형 모델]’ 문구 삭제
+ stepBadgeHtml = '';
+ targetDrawings = [];
} else {
// Requirement 3.1.1: '[3단계: 배근도 기반 3D 철근배근]' 문구 삭제
stepBadgeHtml = '';
@@ -566,7 +550,7 @@ class WingwallApp {
ind.innerHTML = stepBadgeHtml;
tabsContainer.innerHTML = '';
- tabsContainer.style.display = (this.currentStep === 3) ? 'none' : 'flex';
+ tabsContainer.style.display = (this.currentStep === 1) ? 'flex' : 'none';
for (const d of targetDrawings) {
const btn = document.createElement('button');
@@ -829,48 +813,10 @@ class WingwallApp {
// ==================== STEP 2 RENDERERS (일반도 전용 3D 외형 모델링) ====================
renderStep2Sidebar() {
- const genDwgs = this.getGeneralDrawings();
-
- // 1. 적용 일반도 정보 & 체적 요약 카드
- const activeGen = genDwgs.find(d => d.id === this.activeDrawingId) || genDwgs[0] || this.drawingData;
- const genCard = document.getElementById('step2-general-dwg-card');
- if (genCard) {
- if (!activeGen) {
- genCard.innerHTML = `
-
- ⚠ 불러온 일반도가 없습니다.
-
-
- `;
- } else {
- const st = this.modelData?.station || this.params?.station || activeGen.titleBlock?.subTitle || '수로암거';
- const totVol = this.modelData?.summary?.total_volume || 0;
- const wwCount = (this.modelData?.objects?.wingwalls || []).length;
-
- genCard.innerHTML = `
-
-
-
- 📐 ${activeGen.fileName}
-
- 일반도
-
-
- ${st} (날개벽 ${wwCount}개소)
- 총 체적: ${totVol} m³
-
-
- `;
- }
- }
-
- // 2. 평면도 암거 및 날개벽 위치 판별 현황 카드 (심플)
- this.renderStep2DetectionCard(activeGen);
-
- // 3. 외형 모델링 구조물 목록 (심플)
+ // Requirement 1.1: 좌측 바에서 '적용 일반도' 및 '평면도 부재 판별' 테이블 항목 삭제
this.renderStep2StructuresList();
- // 4. Step 3 이동 버튼 이벤트 연결
+ // Step 3 이동 버튼 이벤트 연결
document.getElementById('btn-goto-step3')?.addEventListener('click', () => {
this.setStep(3);
});
@@ -1008,9 +954,10 @@ class WingwallApp {
for (const ww of wwList) {
const c = colorMap[ww.id] || '#38bdf8';
+ const cleanName = (ww.id === 'WW_END_LEFT') ? '종점부 좌측 날개벽' : ((ww.id === 'WW_END_RIGHT') ? '종점부 우측 날개벽' : ww.name);
items.push({
id: ww.id,
- name: ww.name,
+ name: cleanName,
tag: `L=${ww.dimensions.length.toFixed(2)}m (${ww.dimensions.flare_angle.toFixed(0)}°)`,
color: c,
info: `H=${(ww.dimensions.total_height_start || 3.3).toFixed(2)}~${(ww.dimensions.total_height_end || 1.5).toFixed(2)}m · B=${(ww.dimensions.footing_width_start || 2.2).toFixed(2)}~${(ww.dimensions.footing_width_end || 1.5).toFixed(2)}m · ${ww.volume} m³`
@@ -1029,19 +976,20 @@ class WingwallApp {
const card = document.createElement('div');
card.className = `struct-card-step2 ${isSel ? 'selected' : ''}`;
card.id = `step2-struct-${s.id}`;
- card.style.padding = '7px 9px';
- card.style.gap = '3px';
+ card.style.padding = '10px 12px';
+ card.style.gap = '5px';
+ // 1.2: 1.5배 확대 폰트 크기 적용 (.struct-card-step2-title: 17.5px)
card.innerHTML = `
-
+
■
${s.name}
-
${s.tag}
+
${s.tag}
-
+
${s.info}
`;
diff --git a/static/js/viewer3d.js b/static/js/viewer3d.js
index 486729e..4d2caa9 100644
--- a/static/js/viewer3d.js
+++ b/static/js/viewer3d.js
@@ -336,12 +336,12 @@ class Viewer3D {
this.rebarGroupRoot.visible = isStep3;
}
- // Section markers only in Step 2
+ // Section markers and labels hidden to keep 3D model clean (Requirement 2.2.2 & 2.2.3)
for (const line of this.sectionMarkers) {
- line.visible = !isStep3;
+ line.visible = false;
}
for (const sp of this.labelSprites) {
- sp.visible = !isStep3;
+ sp.visible = false;
}
for (const sp of this.rebarLabelSprites) {
sp.visible = isStep3;
@@ -575,44 +575,17 @@ class Viewer3D {
points.push(points[0].clone());
const geom = new THREE.BufferGeometry().setFromPoints(points);
- const mat = new THREE.LineBasicMaterial({ color: s.color, linewidth: 3 });
+ const mat = new THREE.LineBasicMaterial({ color: s.color, linewidth: 2 });
const line = new THREE.Line(geom, mat);
+ line.visible = false;
this.scene.add(line);
this.sectionMarkers.push(line);
-
- const topPt = points[4];
- if (topPt) {
- const sprite = this.createTextSprite(s.name, s.color);
- sprite.position.set(topPt.x, topPt.y, topPt.z + 0.6);
- this.scene.add(sprite);
- this.labelSprites.push(sprite);
- }
+ // Requirement 2.2.3: 단면 0-0 텍스트 표기 삭제 (스프라이트 미생성)
}
}
addAnnotations(objects) {
- const wwL = objects.ww_left;
- const wwR = objects.ww_right;
- if (!wwL || !wwR) return;
-
- const ptA = wwL.sections_3d?.['A-A']?.[5] || [15.725, 42.650, wwL.dimensions.top_el_start];
- const ptB = wwL.sections_3d?.['B-B']?.[5] || [18.75, 45.77, wwL.dimensions.top_el_end];
- const ptD = wwR.sections_3d?.['D-D']?.[5] || [21.01, 38.02, wwR.dimensions.top_el_end];
- const stName = this.modelData?.station || '종점부';
-
- const elMarkers = [
- { text: `EL. ${wwL.dimensions.top_el_start.toFixed(3)} (암거 접합부)`, pos: ptA, color: 0x38bdf8 },
- { text: `EL. ${wwL.dimensions.top_el_end.toFixed(3)} (좌 말단 L=${wwL.dimensions.length}m)`, pos: ptB, color: 0x38bdf8 },
- { text: `EL. ${wwR.dimensions.top_el_end.toFixed(3)} (우 말단 L=${wwR.dimensions.length}m)`, pos: ptD, color: 0xfbbf24 },
- { text: `${stName} (종점부)`, pos: [16.0, 41.65, wwL.dimensions.top_el_start + 0.5], color: 0xffffff }
- ];
-
- for (const m of elMarkers) {
- const sprite = this.createTextSprite(m.text, m.color);
- sprite.position.set(m.pos[0], m.pos[1], m.pos[2] + 0.4);
- this.scene.add(sprite);
- this.labelSprites.push(sprite);
- }
+ // Requirement 2.2.2: 암거본체 선택 시 날개벽 구조물 이름만 표출 (불필요한 EL/스테이션 텍스트 미생성)
}
createTextSprite(text, colorHex) {
@@ -687,18 +660,19 @@ class Viewer3D {
}
updateWingwallNameSpritesVisibility() {
- if (this.stepMode !== 3) {
+ if (this.stepMode !== 2 && this.stepMode !== 3) {
for (const sp of this.wingwallNameSprites) sp.visible = false;
return;
}
- const isCulvert = (!this.selectedWingwallFocus || this.selectedWingwallFocus === 'CULVERT');
+ const currentFocus = (this.stepMode === 3) ? this.selectedWingwallFocus : this.selectedObjectId;
+ const isCulvert = (!currentFocus || currentFocus === 'CULVERT');
for (const sp of this.wingwallNameSprites) {
if (isCulvert) {
- // 암거본체 선택 시 날개벽 구조물의 이름만 표시 (Requirement 3.2.2)
+ // 암거본체 선택 시 날개벽 구조물의 이름만 표시 (Requirement 2.2.2 & 3.2.2)
sp.visible = true;
} else {
// 특정 날개벽 선택 시 해당 날개벽 이름만 표시
- sp.visible = (sp.userData.wingwallId === this.selectedWingwallFocus);
+ sp.visible = (sp.userData.wingwallId === currentFocus);
}
}
}
@@ -717,6 +691,7 @@ class Viewer3D {
mat.emissiveIntensity = 0;
}
}
+ this.updateWingwallNameSpritesVisibility();
}
toggleObjectVisibility(objectId, visible) {
@@ -748,18 +723,34 @@ class Viewer3D {
}
}
- fitView() {
+ // Requirement 2.4: 3D 모델이 메인 화면 표시 영역의 약 80%를 차지하도록 초기 표시 크기(카메라 거리) 조정
+ fitView(coverage = 0.80) {
const box = new THREE.Box3();
for (const id in this.meshes) {
- box.expandByObject(this.meshes[id].group);
+ if (this.meshes[id] && this.meshes[id].group) {
+ box.expandByObject(this.meshes[id].group);
+ }
}
if (box.isEmpty()) return;
const center = box.getCenter(new THREE.Vector3());
- const size = box.getSize(new THREE.Vector3());
- const maxDim = Math.max(size.x, size.y, size.z);
+ const sphere = box.getBoundingSphere(new THREE.Sphere());
+ const radius = Math.max(sphere.radius, 1.0);
+
+ const fov = this.camera.fov * (Math.PI / 180);
+ const aspect = this.camera.aspect || ((this.container && this.container.clientHeight) ? (this.container.clientWidth / this.container.clientHeight) : 1.6);
+
+ const halfFovV = fov / 2;
+ const halfFovH = Math.atan(Math.tan(halfFovV) * aspect);
+ const limitingHalfFov = Math.min(halfFovV, halfFovH);
+
+ // 거리 산출: 바운딩 구가 뷰포트의 제한 치수 대비 약 80%를 차지하도록 배치
+ const targetDist = (radius / coverage) / Math.sin(limitingHalfFov);
+
+ const dir = new THREE.Vector3(1.0, -1.0, 0.78).normalize();
+ const camPos = center.clone().add(dir.multiplyScalar(targetDist));
this.controls.target.copy(center);
- this.camera.position.set(center.x + maxDim * 1.35, center.y - maxDim * 1.35, center.z + maxDim * 1.05);
+ this.camera.position.copy(camPos);
this.camera.lookAt(center);
this.controls.update();
}