diff --git a/README.md b/README.md new file mode 100644 index 0000000..1c7d618 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/run_wsl.sh b/run_wsl.sh new file mode 100644 index 0000000..0cb8a2c --- /dev/null +++ b/run_wsl.sh @@ -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 diff --git a/test_pipeline.py b/test_pipeline.py new file mode 100644 index 0000000..31510aa --- /dev/null +++ b/test_pipeline.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()