From fe6b09f9b2804ba1e816cf6b78c86be81054dab7 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Thu, 22 Oct 2026 14:48:57 +0900 Subject: [PATCH] =?UTF-8?q?feat(ui):=202=EB=8B=A8=EA=B3=84=20=EC=9D=BC?= =?UTF-8?q?=EB=B0=98=EB=8F=84=20=EA=B8=B0=EB=B0=98=203D=20=EC=99=B8?= =?UTF-8?q?=ED=98=95=20=EB=AA=A8=EB=8D=B8=EB=A7=81=20UI=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20=EB=B0=8F=2080%=20=EB=B7=B0=ED=8F=AC=ED=8A=B8=20?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=BC=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.html | 20 ++-------- static/css/style.css | 7 +++- static/js/app.js | 92 ++++++++++--------------------------------- static/js/viewer3d.js | 77 ++++++++++++++++-------------------- 4 files changed, 63 insertions(+), 133 deletions(-) diff --git a/index.html b/index.html index 5e09acb..8646ecb 100644 --- a/index.html +++ b/index.html @@ -137,25 +137,13 @@
- -
- - -
-
-

평면도 부재 판별

- 판별 완료 -
-
-
- - +
-
-

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(); }