Files
geMoldInsight/src/utils/html_generator.py
T
2026-03-07 01:28:03 +08:00

456 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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,
pointcloud_data: 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 = ""
# 强制测试:无论cavity_data如何,都显示面板
if True:
# 安全获取嵌套数据
metadata = cavity_data.get("metadata", {}) if cavity_data else {}
manufacturing_info = cavity_data.get("manufacturing_info", {}) if cavity_data else {}
mold_cavities = cavity_data.get("mold_cavities", {}) if cavity_data else {}
cavity_key_info = mold_cavities.get("cavity_key_info", {})
geo_chars = cavity_key_info.get("geometric_characteristics", {})
logger.info(f"生成HTML - 收缩率: {metadata.get('shrinkage_rate')}")
logger.info(f"生成HTML - 型腔材料: {manufacturing_info.get('mold_material')}")
logger.info(f"生成HTML - 产品体积: {geo_chars.get('product_volume')}")
cavity_html = f"""
<div id="cavity-info-panel" style="position: absolute; top: 10px; right: 10px; background: rgba(255,255,255,0.95); padding: 15px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); max-width: 320px;">
<h3 style="margin: 0 0 10px 0;">🔧 关键工艺参数</h3>
<div style="margin: 10px 0; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
<strong style="color: #333;">模具参数</strong>
</div>
<div class="metric">
<span class="metric-label">收缩率:</span>
<span class="metric-value">{metadata.get("shrinkage_rate", "N/A")}</span>
</div>
<div class="metric">
<span class="metric-label">拔模角:</span>
<span class="metric-value">{metadata.get("draft_angle", "N/A")}°</span>
</div>
<div class="metric">
<span class="metric-label">分型线长度:</span>
<span class="metric-value">{manufacturing_info.get("parting_line_length", "N/A")}</span>
</div>
<div style="margin: 10px 0; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
<strong style="color: #333;">几何特性</strong>
</div>
<div class="metric">
<span class="metric-label">产品体积:</span>
<span class="metric-value">{geo_chars.get("product_volume", "N/A")}</span>
</div>
<div class="metric">
<span class="metric-label">产品重量:</span>
<span class="metric-value">{geo_chars.get("product_weight", "N/A")}</span>
</div>
<div class="metric">
<span class="metric-label">壁厚范围:</span>
<span class="metric-value">{geo_chars.get("wall_thickness_range", "N/A")}</span>
</div>
<div style="margin: 10px 0; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
<strong style="color: #333;">制造要求</strong>
</div>
<div class="metric">
<span class="metric-label">型腔材料:</span>
<span class="metric-value">{manufacturing_info.get("mold_material", "N/A")}</span>
</div>
<div class="metric">
<span class="metric-label">硬度:</span>
<span class="metric-value">{manufacturing_info.get("mold_hardness", "N/A")}</span>
</div>
<div class="metric">
<span class="metric-label">表面光洁度:</span>
<span class="metric-value">{manufacturing_info.get("surface_finish", "N/A")}</span>
</div>
<div class="metric">
<span class="metric-label">预估周期:</span>
<span class="metric-value">{manufacturing_info.get("estimated_cycle_time", "N/A")}</span>
</div>
</div>
"""
else:
logger.warning(f"cavity_data 为空,无法显示工艺参数 - 文件: {stp_filename}")
cavity_html = ""
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>
<button onclick="toggleProduct()">显示/隐藏产品</button>
<button onclick="toggleMold()">显示/隐藏模具</button>
<button onclick="toggleParting()">显示/隐藏分型面</button>
<button onclick="togglePointcloud()">显示/隐藏点云</button>
</div>
</div>
<script>
// 全局变量
let productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh;
let productVisible = true;
let moldVisible = true;
let partingVisible = true;
let pointcloudVisible = true;
// 初始化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 cavityData = {json.dumps(cavity_data, indent=2) if cavity_data else 'null'};
const pointcloudData = {json.dumps(pointcloud_data, indent=2) if pointcloud_data else 'null'};
// 根据边界框创建几何体
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;
// 如果有点云数据,创建点云模型
if (pointcloudData && pointcloudData.points && pointcloudData.points.length > 0) {{
// 创建点云几何体
const pointGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(pointcloudData.points.flat());
pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
// 添加法向量(如果有)
if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
const normals = new Float32Array(pointcloudData.normals.flat());
pointGeometry.setAttribute('normal', new THREE.BufferAttribute(normals, 3));
}}
// 创建点云材质
const pointMaterial = new THREE.PointsMaterial({{
color: 0x4CAF50,
size: 0.5,
transparent: true,
opacity: 0.8
}});
pointcloudMesh = new THREE.Points(pointGeometry, pointMaterial);
scene.add(pointcloudMesh);
// 计算中心点并调整相机位置
const center = new THREE.Vector3();
pointGeometry.computeBoundingBox();
pointGeometry.boundingBox.getCenter(center);
controls.target.set(center.x, center.y, center.z);
}} else {{
// 创建产品几何体(半透明绿色)
const productGeometry = new THREE.BoxGeometry(width * 0.9, height * 0.9, depth * 0.9);
const productMaterial = new THREE.MeshPhongMaterial({{
color: 0x4CAF50,
transparent: true,
opacity: 0.6,
wireframe: false
}});
productMesh = new THREE.Mesh(productGeometry, productMaterial);
productMesh.position.set(0, 0, 0);
scene.add(productMesh);
}}
// 创建型腔(上半模 - 蓝色)
const cavityGeometry = new THREE.BoxGeometry(width * 1.1, height * 0.3, depth * 1.1);
const cavityMaterial = new THREE.MeshPhongMaterial({{
color: 0x2196F3,
transparent: true,
opacity: 0.4,
wireframe: false
}});
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
cavityMesh.position.set(0, height * 0.6, 0);
scene.add(cavityMesh);
// 创建型芯(下半模 - 橙色)
const coreGeometry = new THREE.BoxGeometry(width * 1.1, height * 0.3, depth * 1.1);
const coreMaterial = new THREE.MeshPhongMaterial({{
color: 0xFF9800,
transparent: true,
opacity: 0.4,
wireframe: false
}});
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
coreMesh.position.set(0, -height * 0.6, 0);
scene.add(coreMesh);
// 创建分型面(红色平面)
const partingGeometry = new THREE.PlaneGeometry(width * 1.2, depth * 1.2);
const partingMaterial = new THREE.MeshBasicMaterial({{
color: 0xF44336,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide
}});
partingMesh = new THREE.Mesh(partingGeometry, partingMaterial);
partingMesh.rotation.x = Math.PI / 2;
partingMesh.position.set(0, 0, 0);
scene.add(partingMesh);
// 添加线框
if (productMesh) {{
const productWireframe = new THREE.WireframeGeometry(productMesh.geometry);
const productLine = new THREE.LineSegments(productWireframe);
productLine.material.depthTest = false;
productLine.material.opacity = 0.5;
productLine.material.transparent = true;
productLine.material.color = new THREE.Color(0x2E7D32);
productMesh.add(productLine);
}}
const cavityWireframe = new THREE.WireframeGeometry(cavityGeometry);
const cavityLine = new THREE.LineSegments(cavityWireframe);
cavityLine.material.depthTest = false;
cavityLine.material.opacity = 0.6;
cavityLine.material.transparent = true;
cavityLine.material.color = new THREE.Color(0x1565C0);
cavityMesh.add(cavityLine);
const coreWireframe = new THREE.WireframeGeometry(coreGeometry);
const coreLine = new THREE.LineSegments(coreWireframe);
coreLine.material.depthTest = false;
coreLine.material.opacity = 0.6;
coreLine.material.transparent = true;
coreLine.material.color = new THREE.Color(0xE65100);
coreMesh.add(coreLine);
}}
// 设置相机位置
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;
}}
}});
}}
function toggleProduct() {{
if (productMesh) {{
productVisible = !productVisible;
productMesh.visible = productVisible;
}}
}}
function toggleMold() {{
if (cavityMesh && coreMesh) {{
moldVisible = !moldVisible;
cavityMesh.visible = moldVisible;
coreMesh.visible = moldVisible;
}}
}}
function toggleParting() {{
if (partingMesh) {{
partingVisible = !partingVisible;
partingMesh.visible = partingVisible;
}}
}}
function togglePointcloud() {{
if (pointcloudMesh) {{
pointcloudVisible = !pointcloudVisible;
pointcloudMesh.visible = pointcloudVisible;
}}
}}
</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,
cavity_data: Optional[Dict[str, Any]] = None,
pointcloud_data: Optional[Dict[str, Any]] = None
) -> str:
"""生成并保存可视化HTML文件"""
try:
# 生成HTML内容
html_content = self.generate_3d_viewer_html(
geometry_data,
stp_filename,
cavity_data=cavity_data,
pointcloud_data=pointcloud_data
)
# 创建文件名
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