feat(wsl2): add WSL2 execution script, automated test suite and documentation
This commit is contained in:
1 parent
e5abc6f591
commit
aa3ae545fd
3 files changed
+274
No files matched your search
@@ -0,0 +1,71 @@
|
||||
# 2D 도면 기반 3D 수로암거 및 옹벽형 날개벽 모델링 웹 시스템 (Code/3d-drawing)
|
||||
|
||||
2D CAD 도면(일반도 및 구조도/배근도 DWG/DXF 5종)을 분석하여 수로암거(`STA.13+611.21`) 및 옹벽형 날개벽의 3D 솔리드 외형과 3D 내부 철근 배근(19종 508본)을 자동 구축하고, Three.js 기반의 웹 뷰어를 통해 인터랙티브하게 확인·검증·내보내기하는 시스템입니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 주요 특징 및 구현 내용
|
||||
|
||||
- **1단계: 도면 불러오기 및 도면 종류 판별**
|
||||
- DWG/DXF 파일 파싱 (`ezdwg`, `ezdxf`)
|
||||
- 도면명 키워드 자동 판별: `'일반'` ➔ 일반도, `'배근'`/`'구조'` ➔ 구조도/배근도
|
||||
- 2D CAD 벡터 뷰어 및 레이어 제어 지원
|
||||
- **2단계: 일반도 기반 3D 외형 모델링**
|
||||
- 암거 본체 및 날개벽 위치 판별 (시점부 좌/우, 종점부 좌/우)
|
||||
- 본 도면 특성 완벽 준수: 시점부 미표기 ➔ 임의 생성 배제 / 종점부 좌·우 2개소 모델링 ($L=4.35\text{m}$, $L=5.40\text{m}$)
|
||||
- 완전 폐합 2-Manifold 수밀성(Watertight) 3D 솔리드 메시 생성 (총 콘크리트 체적: $75.45\text{ m}^3$)
|
||||
- **3단계: 배근도 기반 3D 철근 배근 모델링**
|
||||
- 피복두께 80mm 준수
|
||||
- 19종 철근 기호, 총 508본 3D 입체 배근 (종점 좌 231본 + 종점 우 277본)
|
||||
- 철근 재료표(BBS) 자동 산출 (총중량: $1.543\text{ TON}$, 할증 포함 $1.590\text{ TON}$)
|
||||
- **웹 3D 뷰어 (Three.js)**
|
||||
- 3차원 자유 시점 궤도 제어 (OrbitControls)
|
||||
- 암거 본체 / 날개벽 / 철근 레이어별 가시화 및 투명도 제어
|
||||
- 3D 모델 OBJ, STL, DXF 내보내기
|
||||
|
||||
---
|
||||
|
||||
## 2. 작업 환경 및 디렉터리 구성
|
||||
|
||||
- **작업 환경**: WSL2 (Ubuntu Linux)
|
||||
- **저장소 위치**: `Code/3d-drawing/` (Git 관리)
|
||||
|
||||
```text
|
||||
Code/3d-drawing/
|
||||
├── cad_parser.py # 2D DWG/DXF 도면 파싱 및 종류 판별
|
||||
├── model_3d.py # 3D 수로암거 및 날개벽 파라메트릭 솔리드 모델러
|
||||
├── rebar_model.py # 3D 철근 배근 생성기 (19종 기호, 508본)
|
||||
├── exporters.py # OBJ / STL / DXF 3D 파일 내보내기 모듈
|
||||
├── bundle_data.py # 도면 및 3D 데이터 번들링 유틸리티
|
||||
├── app.py # Flask 웹 REST API 서버
|
||||
├── index.html # Three.js 기반 3D CAD/BIM 웹 뷰어
|
||||
├── static/ # CSS, JS(app.js, viewer3d.js), Three.js 라이브러리 및 데이터
|
||||
├── run_wsl.sh # WSL2 원클릭 실행 스크립트
|
||||
├── test_pipeline.py # 파이프라인 전수 자동화 검증 테스트
|
||||
├── requirements.txt # Python 패키지 의존성
|
||||
└── README.md # 본 설명서
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. WSL2 실행 방법
|
||||
|
||||
WSL2(Ubuntu) 터미널에서 아래 명령을 실행합니다:
|
||||
|
||||
```bash
|
||||
cd Code/3d-drawing
|
||||
chmod +x run_wsl.sh
|
||||
./run_wsl.sh
|
||||
```
|
||||
|
||||
실행 후 웹 브라우저에서 아래 주소로 접속합니다:
|
||||
- **`http://localhost:5000`** 또는 **`http://127.0.0.1:5000`**
|
||||
|
||||
---
|
||||
|
||||
## 4. 자동 테스트 및 검증 실행
|
||||
|
||||
```bash
|
||||
cd Code/3d-drawing
|
||||
~/.venv-3d-drawing/bin/python test_pipeline.py
|
||||
```
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# ==============================================================================
|
||||
# DWG 도면 기반 3D 모델링 및 웹 뷰어 실행 스크립트 (WSL2 / Ubuntu 환경)
|
||||
# ==============================================================================
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
VENV_PATH="$HOME/.venv-3d-drawing"
|
||||
|
||||
echo "======================================================================"
|
||||
echo " [3D Drawing Converter & Viewer] WSL2(Ubuntu) 실행 환경 초기화"
|
||||
echo "======================================================================"
|
||||
|
||||
# 1. 가상환경 확인 및 필요 시 자동 생성
|
||||
if [ ! -f "$VENV_PATH/bin/python" ]; then
|
||||
echo ">> 가상환경이 존재하지 않아 생성 중: $VENV_PATH"
|
||||
python3 -m venv "$VENV_PATH"
|
||||
"$VENV_PATH/bin/pip" install --upgrade pip
|
||||
"$VENV_PATH/bin/pip" install -r requirements.txt
|
||||
fi
|
||||
|
||||
echo ">> Python 가상환경 활성화: $VENV_PATH"
|
||||
source "$VENV_PATH/bin/activate"
|
||||
|
||||
# 2. 필수 의존성 검증
|
||||
python -c "import flask, flask_cors, ezdxf, ezdwg, numpy; print('>> 모든 필수 패키지 검증 완료 (flask, ezdxf, ezdwg, numpy)')"
|
||||
|
||||
# 3. Flask 웹 서버 기동
|
||||
PORT=${PORT:-5000}
|
||||
HOST=${HOST:-0.0.0.0}
|
||||
|
||||
echo "----------------------------------------------------------------------"
|
||||
echo ">> 3D 도면 뷰어 웹 서버를 실행합니다..."
|
||||
echo ">> 접속 주소: http://localhost:$PORT / http://127.0.0.1:$PORT"
|
||||
echo ">> 종료하려면 Ctrl+C 를 누르세요."
|
||||
echo "----------------------------------------------------------------------"
|
||||
|
||||
exec python app.py
|
||||
@@ -0,0 +1,163 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive End-to-End Pipeline Verification Test
|
||||
Verifies 2D drawing classification, DWG parsing, 3D solid & rebar modeling,
|
||||
mesh watertightness, exporter functionality, and Flask API endpoints.
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import unittest
|
||||
|
||||
import cad_parser
|
||||
import model_3d
|
||||
import rebar_model
|
||||
import exporters
|
||||
import app
|
||||
|
||||
|
||||
class Test3DDrawingPipeline(unittest.TestCase):
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
cls.project_root = os.path.abspath(os.path.join(current_dir, '..', '..'))
|
||||
cls.rag_dir = os.path.join(cls.project_root, 'RAG')
|
||||
cls.general_dwg = os.path.join(cls.rag_dir, '13. 수로암거 일반도(STA.13+611.21)(02).dwg')
|
||||
app.app.testing = True
|
||||
cls.flask_app = app.app.test_client()
|
||||
|
||||
def test_01_drawing_classification(self):
|
||||
"""1.2 도면 종류 키워드 기반 자동 판별 검증"""
|
||||
# 일반도 판별
|
||||
res_gen = cad_parser.classify_drawing_by_name('13. 수로암거 일반도(STA.13+611.21)(02).dwg')
|
||||
self.assertEqual(res_gen['auto_type'], 'GENERAL')
|
||||
self.assertEqual(res_gen['matched_keyword'], '일반')
|
||||
self.assertFalse(res_gen['needs_manual_selection'])
|
||||
|
||||
# 배근도/구조도 판별
|
||||
res_rebar1 = cad_parser.classify_drawing_by_name('14. 수로암거 구조도(STA.13+611.21, 종점 우)(01).dwg')
|
||||
self.assertEqual(res_rebar1['auto_type'], 'REBAR')
|
||||
self.assertIn('구조', res_rebar1['matched_keyword'])
|
||||
self.assertFalse(res_rebar1['needs_manual_selection'])
|
||||
|
||||
res_rebar2 = cad_parser.classify_drawing_by_name('날개벽 배근도(종점 좌).dwg')
|
||||
self.assertEqual(res_rebar2['auto_type'], 'REBAR')
|
||||
self.assertIn('배근', res_rebar2['matched_keyword'])
|
||||
|
||||
# 미분류 도면
|
||||
res_unclass = cad_parser.classify_drawing_by_name('기타 도면#01.dwg')
|
||||
self.assertEqual(res_unclass['auto_type'], 'UNCLASSIFIED')
|
||||
self.assertTrue(res_unclass['needs_manual_selection'])
|
||||
|
||||
def test_02_dwg_parsing(self):
|
||||
"""1.1 DWG 도면 파싱 및 파라미터 추출 검증"""
|
||||
if not os.path.exists(self.general_dwg):
|
||||
self.skipTest(f"DWG file not found at {self.general_dwg}")
|
||||
|
||||
drawing_data = cad_parser.parse_dwg(self.general_dwg)
|
||||
self.assertIsNotNone(drawing_data)
|
||||
self.assertIn('entities', drawing_data)
|
||||
self.assertIn('structuralParams', drawing_data)
|
||||
self.assertGreater(len(drawing_data['entities']), 100)
|
||||
|
||||
# 수로암거 제원 확인
|
||||
sp = drawing_data['structuralParams']
|
||||
self.assertEqual(sp.get('station'), 'STA.13+611.21')
|
||||
self.assertAlmostEqual(sp.get('culvert_span', 2.0), 2.0, delta=0.1)
|
||||
|
||||
def test_03_3d_solid_modeling_and_watertightness(self):
|
||||
"""2단계 일반도 기반 3D 외형 모델링 및 수밀성(Watertight) 검증"""
|
||||
model_res = model_3d.generate_full_model()
|
||||
self.assertIsNotNone(model_res)
|
||||
self.assertIn('objects', model_res)
|
||||
|
||||
objects = model_res['objects']
|
||||
self.assertIn('culvert', objects)
|
||||
self.assertIn('ww_end_left', objects)
|
||||
self.assertIn('ww_end_right', objects)
|
||||
|
||||
# 도면에 없는 시점부 날개벽 임의 생성 차단 확인
|
||||
self.assertNotIn('ww_start_left', objects)
|
||||
self.assertNotIn('ww_start_right', objects)
|
||||
|
||||
# 수밀성 (Watertight 2-Manifold Mesh) 검증
|
||||
for part_key in ['culvert', 'ww_end_left', 'ww_end_right']:
|
||||
part_data = objects[part_key]
|
||||
vertices = part_data['vertices']
|
||||
faces = part_data['faces']
|
||||
self.assertGreater(len(vertices), 0, f"{part_key} has no vertices")
|
||||
self.assertGreater(len(faces), 0, f"{part_key} has no faces")
|
||||
|
||||
# Edge manifold check: Every edge in a watertight 2-manifold closed surface is shared by exactly 2 faces
|
||||
edge_count = {}
|
||||
for f in faces:
|
||||
for i in range(3):
|
||||
v1, v2 = f[i], f[(i + 1) % 3]
|
||||
edge = tuple(sorted([v1, v2]))
|
||||
edge_count[edge] = edge_count.get(edge, 0) + 1
|
||||
|
||||
non_manifold = [e for e, count in edge_count.items() if count != 2]
|
||||
self.assertEqual(len(non_manifold), 0,
|
||||
f"{part_key} mesh is not watertight! Non-manifold edges: {len(non_manifold)}")
|
||||
|
||||
def test_04_3d_rebar_modeling(self):
|
||||
"""3단계 구조도/배근도 기반 날개벽 내부 3D 철근 배근(508본) 검증"""
|
||||
rebar_data = rebar_model.generate_full_rebar_model()
|
||||
self.assertIsNotNone(rebar_data)
|
||||
self.assertIn('wingwalls', rebar_data)
|
||||
self.assertIn('summary', rebar_data)
|
||||
|
||||
summary = rebar_data['summary']
|
||||
total_bars = summary.get('total_bars', 0)
|
||||
self.assertEqual(total_bars, 508, f"Expected 508 total rebars, got {total_bars}")
|
||||
|
||||
# 좌측(231본) 및 우측(277본) 검증
|
||||
left_bars = summary.get('left_bars', 0)
|
||||
right_bars = summary.get('right_bars', 0)
|
||||
self.assertEqual(left_bars, 231)
|
||||
self.assertEqual(right_bars, 277)
|
||||
|
||||
def test_05_exporters(self):
|
||||
"""3D 모델 OBJ, STL, DXF 내보내기 검증"""
|
||||
model_res = model_3d.generate_full_model()
|
||||
|
||||
obj_str = exporters.export_obj(model_res)
|
||||
self.assertIn('v ', obj_str)
|
||||
self.assertIn('f ', obj_str)
|
||||
|
||||
stl_str = exporters.export_stl(model_res)
|
||||
self.assertIn('solid ', stl_str)
|
||||
self.assertIn('facet normal', stl_str)
|
||||
|
||||
dxf_str = exporters.export_dxf_3d(model_res)
|
||||
self.assertIn('SECTION', dxf_str)
|
||||
self.assertIn('ENTITIES', dxf_str)
|
||||
|
||||
csv_str = exporters.export_rebar_bom_csv(model_res)
|
||||
self.assertIn('구분(날개벽)', csv_str)
|
||||
self.assertIn('기호(Mark)', csv_str)
|
||||
|
||||
def test_06_flask_web_endpoints(self):
|
||||
"""Flask API 및 웹 엔드포인트 응답 검증"""
|
||||
# 1. 메인 웹페이지
|
||||
r_index = self.flask_app.get('/')
|
||||
self.assertEqual(r_index.status_code, 200)
|
||||
|
||||
# 2. 기본 도면 데이터 조회 API
|
||||
r_def = self.flask_app.get('/api/default-drawing')
|
||||
self.assertEqual(r_def.status_code, 200)
|
||||
data_def = json.loads(r_def.data)
|
||||
self.assertEqual(data_def.get('status'), 'success')
|
||||
|
||||
# 3. 도면 분류 API
|
||||
r_cls = self.flask_app.post('/api/classify',
|
||||
data=json.dumps({'drawingName': '13. 수로암거 일반도.dwg'}),
|
||||
content_type='application/json')
|
||||
self.assertEqual(r_cls.status_code, 200)
|
||||
data_cls = json.loads(r_cls.data)
|
||||
self.assertEqual(data_cls['classification']['selectedType'], 'GENERAL')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user