Files
geMoldInsight/src/utils/html_generator.py
T
2026-04-21 15:22:22 +08:00

649 lines
28 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()">显示/隐藏A/B板</button>
<button onclick="toggleParting()">显示/隐藏分型面</button>
<button onclick="togglePointcloud()">显示/隐藏点云</button>
<button id="splitBtn" onclick="splitMold()" style="background:#FF5722;color:#fff;font-weight:bold;">分模拆分</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 controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
// 创建几何体(使用点云数据或模拟模具形状)
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: 1.0,
transparent: true,
opacity: 0.9
}});
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);
// 根据边界框大小调整相机距离
const boundingSphere = new THREE.Sphere();
pointGeometry.boundingBox.getBoundingSphere(boundingSphere);
const cameraDistance = boundingSphere.radius * 3;
camera.position.set(cameraDistance, cameraDistance, cameraDistance);
}} 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);
}}
// 创建A板/定模(蓝色,分型面以上)
// 尝试从后端数据获取实际几何,否则使用简化Box
if (cavityData && cavityData.mold_cavities && cavityData.mold_cavities.cavity && cavityData.mold_cavities.cavity.vertices) {{
const cavityVerts = cavityData.mold_cavities.cavity.vertices;
const cavityFaces = cavityData.mold_cavities.cavity.faces;
if (cavityVerts.length > 0 && cavityFaces.length > 0) {{
const cavityGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(cavityVerts);
const indices = new Uint32Array(cavityFaces);
cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
cavityGeometry.computeVertexNormals();
const cavityMaterial = new THREE.MeshPhongMaterial({{
color: 0x2196F3,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide
}});
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
scene.add(cavityMesh);
// 添加线框
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);
// 设置相机目标为型腔中心
cavityGeometry.computeBoundingBox();
const cavityCenter = new THREE.Vector3();
cavityGeometry.boundingBox.getCenter(cavityCenter);
controls.target.set(cavityCenter.x, cavityCenter.y, cavityCenter.z);
}} else {{
createSimpleCavity(width, height, depth);
}}
}} else {{
createSimpleCavity(width, height, depth);
}}
// 创建B板/动模(橙色,分型面以下)
if (cavityData && cavityData.mold_cavities && cavityData.mold_cavities.core && cavityData.mold_cavities.core.vertices) {{
const coreVerts = cavityData.mold_cavities.core.vertices;
const coreFaces = cavityData.mold_cavities.core.faces;
if (coreVerts.length > 0 && coreFaces.length > 0) {{
const coreGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(coreVerts);
const indices = new Uint32Array(coreFaces);
coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
coreGeometry.computeVertexNormals();
const coreMaterial = new THREE.MeshPhongMaterial({{
color: 0xFF9800,
transparent: true,
opacity: 0.5,
wireframe: false,
side: THREE.DoubleSide
}});
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
scene.add(coreMesh);
// 添加线框
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);
}} else {{
createSimpleCore(width, height, depth);
}}
}} else {{
createSimpleCore(width, height, depth);
}}
// 创建分型面(红色平面)
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);
}}
}}
// 创建简化型腔/A板(定模,分型面以上)— 备用
function createSimpleCavity(width, height, depth) {{
// A板:分型面(Z中心)以上的上半模
const halfHeight = height / 2;
const cavityGeometry = new THREE.BoxGeometry(width * 1.2, depth * 1.2, halfHeight + 10);
const cavityMaterial = new THREE.MeshPhongMaterial({{
color: 0x2196F3,
transparent: true,
opacity: 0.4,
wireframe: false
}});
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
// Z轴开模:A板放在分型面以上
cavityMesh.position.set(0, 0, halfHeight / 2 + 5);
scene.add(cavityMesh);
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);
}}
// 创建简化型芯/B板(动模,分型面以下)— 备用
function createSimpleCore(width, height, depth) {{
// B板:分型面(Z中心)以下的下半模
const halfHeight = height / 2;
const coreGeometry = new THREE.BoxGeometry(width * 1.2, depth * 1.2, halfHeight + 10);
const coreMaterial = new THREE.MeshPhongMaterial({{
color: 0xFF9800,
transparent: true,
opacity: 0.4,
wireframe: false
}});
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
// Z轴开模:B板放在分型面以下
coreMesh.position.set(0, 0, -halfHeight / 2 - 5);
scene.add(coreMesh);
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);
// 动画循环
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;
}}
}}
// ─── 分模拆分动画 ───
let isSplit = false;
let splitAnimId = null;
// 根据材料类型确定分模方向
function getPartingDirection() {{
// 优先使用后端传递的 parting_direction
if (cavityData?.metadata?.parting_direction) {{
return cavityData.metadata.parting_direction;
}}
if (cavityData?.manufacturing_info?.parting_direction) {{
return cavityData.manufacturing_info.parting_direction;
}}
// 泡沫材料默认 Z 轴
if (cavityData?.metadata?.is_foam) {{
return 'Z';
}}
// 默认 Z 轴
return 'Z';
}}
function splitMold() {{
if (!cavityMesh && !coreMesh) return;
isSplit = !isSplit;
const btn = document.getElementById('splitBtn');
btn.textContent = isSplit ? '合模' : '分模拆分';
const bboxDims = geometryData.bounding_box?.dimensions || [100, 100, 100];
const dir = getPartingDirection();
// 根据分模方向确定拆分轴和距离
let splitDist, axis;
if (dir === 'Z') {{
// Z轴上下开模:拆分距离取 Z 方向高度的 40%
splitDist = bboxDims[2] * 0.4;
axis = 'z';
}} else if (dir === 'Y') {{
// Y轴前后开模
splitDist = bboxDims[1] * 0.4;
axis = 'y';
}} else {{
// X轴左右开模
splitDist = bboxDims[0] * 0.4;
axis = 'x';
}}
// 分型面动画目标透明度
const partingTargetOpacity = isSplit ? 0 : 0.3;
// 记录起始位置
const cavityStart = cavityMesh ? cavityMesh.position[axis] : 0;
const coreStart = coreMesh ? coreMesh.position[axis] : 0;
const partingStartOpacity = partingMesh ? partingMesh.material.opacity : 0.3;
// 目标位置:型腔正向移动,型芯负向移动
const cavityTarget = isSplit ? splitDist : 0;
const coreTarget = isSplit ? -splitDist : 0;
const duration = 800; // ms
const startTime = performance.now();
if (splitAnimId) cancelAnimationFrame(splitAnimId);
function animateSplit(now) {{
const elapsed = now - startTime;
const t = Math.min(elapsed / duration, 1);
// 缓动函数 (easeInOutCubic)
const ease = t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
if (cavityMesh) {{
cavityMesh.position[axis] = cavityStart + (cavityTarget - cavityStart) * ease;
}}
if (coreMesh) {{
coreMesh.position[axis] = coreStart + (coreTarget - coreStart) * ease;
}}
if (partingMesh) {{
partingMesh.material.opacity = partingStartOpacity + (partingTargetOpacity - partingStartOpacity) * ease;
if (isSplit && t >= 1) partingMesh.visible = false;
else partingMesh.visible = true;
}}
if (t < 1) {{
splitAnimId = requestAnimationFrame(animateSplit);
}} else {{
splitAnimId = null;
}}
}}
splitAnimId = requestAnimationFrame(animateSplit);
}}
</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