init
This commit is contained in:
@@ -0,0 +1 @@
|
||||
# Utils 模块
|
||||
@@ -0,0 +1,28 @@
|
||||
# utils/file_handler.py
|
||||
import aiofiles
|
||||
from pathlib import Path
|
||||
from fastapi import UploadFile
|
||||
|
||||
|
||||
class FileHandler:
|
||||
def __init__(self, upload_dir: str = "uploads"):
|
||||
self.upload_dir = Path(upload_dir)
|
||||
self.upload_dir.mkdir(exist_ok=True)
|
||||
|
||||
async def save_uploaded_file(self, file: UploadFile) -> Path:
|
||||
"""保存上传的文件"""
|
||||
file_path = self.upload_dir / file.filename
|
||||
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
content = await file.read()
|
||||
await f.write(content)
|
||||
|
||||
return file_path
|
||||
|
||||
def cleanup_file(self, file_path: Path):
|
||||
"""清理文件"""
|
||||
try:
|
||||
if file_path.exists():
|
||||
file_path.unlink()
|
||||
except Exception as e:
|
||||
print(f"文件清理失败: {e}")
|
||||
@@ -0,0 +1,279 @@
|
||||
# utils/html_generator.py
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, Optional
|
||||
import json
|
||||
from datetime import datetime
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
class HTMLGenerator:
|
||||
"""HTML文件生成器"""
|
||||
|
||||
def __init__(self, output_dir: str = "./html_output"):
|
||||
self.output_dir = Path(output_dir)
|
||||
self.output_dir.mkdir(exist_ok=True)
|
||||
|
||||
def generate_3d_viewer_html(
|
||||
self,
|
||||
geometry_data: Dict[str, Any],
|
||||
stp_filename: str,
|
||||
cavity_data: Optional[Dict[str, Any]] = None,
|
||||
key_info: Optional[Dict[str, Any]] = None
|
||||
) -> str:
|
||||
"""生成3D可视化HTML页面"""
|
||||
|
||||
# 提取几何数据
|
||||
bounding_box = geometry_data.get("bounding_box", {})
|
||||
volume = geometry_data.get("volume", 0) or 0
|
||||
surface_area = geometry_data.get("surface_area", 0) or 0
|
||||
topology = geometry_data.get("topology", {})
|
||||
center_of_mass = geometry_data.get("center_of_mass", [0, 0, 0])
|
||||
cavity_html = ""
|
||||
if cavity_data and key_info:
|
||||
cavity_html = f"""
|
||||
<div id="cavity-info-panel" style="position: absolute; top: 10px; right: 10px;">
|
||||
<h3>🔧 模具型腔信息</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">收缩率:</span>
|
||||
<span class="metric-value">{cavity_data["metadata"]["shrinkage_rate"]}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">拔模角:</span>
|
||||
<span class="metric-value">{cavity_data["metadata"]["draft_angle"]}°</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">预估模具尺寸:</span>
|
||||
<span class="metric-value">
|
||||
{key_info["mold_parameters"]["mold_size"]["length"]:.0f} ×
|
||||
{key_info["mold_parameters"]["mold_size"]["width"]:.0f} ×
|
||||
{key_info["mold_parameters"]["mold_size"]["height"]:.0f} mm
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">预估锁模力:</span>
|
||||
<span class="metric-value">
|
||||
{key_info["manufacturing_requirements"]["clamping_force"]}
|
||||
</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">产品重量:</span>
|
||||
<span class="metric-value">
|
||||
{key_info["geometric_characteristics"]["product_weight"]}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
html_content = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>3D模具几何可视化 - {stp_filename}</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/build/three.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.132.2/examples/js/controls/OrbitControls.js"></script>
|
||||
<style>
|
||||
body {{ margin: 0; overflow: hidden; font-family: Arial, sans-serif; }}
|
||||
#container {{ position: relative; width: 100vw; height: 100vh; }}
|
||||
#canvas {{ display: block; }}
|
||||
#info-panel {{
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
max-width: 300px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}}
|
||||
#controls {{
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
}}
|
||||
.metric {{ margin: 5px 0; }}
|
||||
.metric-label {{ font-weight: bold; color: #333; }}
|
||||
.metric-value {{ color: #666; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<canvas id="canvas"></canvas>
|
||||
{cavity_html}
|
||||
<div id="info-panel">
|
||||
<h3>模具几何信息</h3>
|
||||
<div class="metric">
|
||||
<span class="metric-label">文件名:</span>
|
||||
<span class="metric-value">{stp_filename}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">体积:</span>
|
||||
<span class="metric-value">{volume:.2f} mm³</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">表面积:</span>
|
||||
<span class="metric-value">{surface_area:.2f} mm²</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">边界框:</span>
|
||||
<span class="metric-value">{bounding_box.get('dimensions', [0, 0, 0])[0]:.1f} × {bounding_box.get('dimensions', [0, 0, 0])[1]:.1f} × {bounding_box.get('dimensions', [0, 0, 0])[2]:.1f} mm</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">面数:</span>
|
||||
<span class="metric-value">{topology.get('faces', 0)}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">边数:</span>
|
||||
<span class="metric-value">{topology.get('edges', 0)}</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span class="metric-label">顶点数:</span>
|
||||
<span class="metric-value">{topology.get('vertices', 0)}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="controls">
|
||||
<button onclick="resetView()">重置视图</button>
|
||||
<button onclick="toggleWireframe()">切换线框</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 初始化Three.js场景
|
||||
const scene = new THREE.Scene();
|
||||
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
const renderer = new THREE.WebGLRenderer({{ canvas: document.getElementById('canvas') }});
|
||||
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.setClearColor(0xf0f0f0);
|
||||
|
||||
// 添加光源
|
||||
const ambientLight = new THREE.AmbientLight(0x404040);
|
||||
scene.add(ambientLight);
|
||||
|
||||
const directionalLight = new THREE.DirectionalLight(0xffffff, 1);
|
||||
directionalLight.position.set(1, 1, 1);
|
||||
scene.add(directionalLight);
|
||||
|
||||
// 添加坐标轴
|
||||
const axesHelper = new THREE.AxesHelper(50);
|
||||
scene.add(axesHelper);
|
||||
|
||||
// 创建几何体(模拟模具形状)
|
||||
const geometryData = {json.dumps(geometry_data, indent=2)};
|
||||
|
||||
// 根据边界框创建模拟几何体
|
||||
const bbox = geometryData.bounding_box;
|
||||
if (bbox) {{
|
||||
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
|
||||
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
|
||||
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
|
||||
|
||||
// 创建基础几何体
|
||||
const geometry = new THREE.BoxGeometry(width, height, depth);
|
||||
const material = new THREE.MeshPhongMaterial({{
|
||||
color: 0x4CAF50,
|
||||
transparent: true,
|
||||
opacity: 0.8,
|
||||
wireframe: false
|
||||
}});
|
||||
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
scene.add(mesh);
|
||||
|
||||
// 添加线框
|
||||
const wireframe = new THREE.WireframeGeometry(geometry);
|
||||
const line = new THREE.LineSegments(wireframe);
|
||||
line.material.depthTest = false;
|
||||
line.material.opacity = 0.25;
|
||||
line.material.transparent = true;
|
||||
scene.add(line);
|
||||
}}
|
||||
|
||||
// 设置相机位置
|
||||
camera.position.set(200, 200, 200);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
// 添加轨道控制器
|
||||
const controls = new THREE.OrbitControls(camera, renderer.domElement);
|
||||
controls.enableDamping = true;
|
||||
controls.dampingFactor = 0.25;
|
||||
|
||||
// 动画循环
|
||||
function animate() {{
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
renderer.render(scene, camera);
|
||||
}}
|
||||
|
||||
animate();
|
||||
|
||||
// 窗口大小调整
|
||||
window.addEventListener('resize', () => {{
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}});
|
||||
|
||||
// 控制函数
|
||||
function resetView() {{
|
||||
controls.reset();
|
||||
}}
|
||||
|
||||
function toggleWireframe() {{
|
||||
scene.traverse((child) => {{
|
||||
if (child.isMesh) {{
|
||||
child.material.wireframe = !child.material.wireframe;
|
||||
}}
|
||||
}});
|
||||
}}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
return html_content
|
||||
|
||||
def save_html_file(self, html_content: str, filename: str) -> str:
|
||||
"""保存HTML文件到磁盘"""
|
||||
try:
|
||||
file_path = self.output_dir / filename
|
||||
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
f.write(html_content)
|
||||
|
||||
logger.info(f"HTML文件保存成功: {file_path}")
|
||||
return str(file_path)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"保存HTML文件失败: {e}")
|
||||
raise
|
||||
|
||||
def generate_and_save_visualization(
|
||||
self,
|
||||
geometry_data: Dict[str, Any],
|
||||
stp_filename: str
|
||||
) -> str:
|
||||
"""生成并保存可视化HTML文件"""
|
||||
try:
|
||||
# 生成HTML内容
|
||||
html_content = self.generate_3d_viewer_html(geometry_data, stp_filename)
|
||||
|
||||
# 创建文件名
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
safe_filename = stp_filename.replace('.', '_').replace(' ', '_')
|
||||
html_filename = f"{safe_filename}_{timestamp}.html"
|
||||
|
||||
# 保存文件
|
||||
file_path = self.save_html_file(html_content, html_filename)
|
||||
|
||||
return file_path
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"生成可视化文件失败: {e}")
|
||||
raise
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
# utils/logger.py
|
||||
import logging
|
||||
import sys
|
||||
|
||||
def setup_logging():
|
||||
"""设置日志配置"""
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(sys.stdout)
|
||||
]
|
||||
)
|
||||
|
||||
def get_logger(name: str):
|
||||
"""获取日志器"""
|
||||
return logging.getLogger(name)
|
||||
Reference in New Issue
Block a user