Files
geMoldInsight/src/utils/html_generator.py
T

797 lines
36 KiB
Python
Raw Normal View History

2026-02-11 22:40:35 +08:00
# 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,
2026-03-07 01:18:25 +08:00
pointcloud_data: Optional[Dict[str, Any]] = None
2026-02-11 22:40:35 +08:00
) -> 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 = ""
2026-02-15 01:09:53 +08:00
# 强制测试:无论cavity_data如何,都显示面板
if True:
2026-02-15 01:01:03 +08:00
# 安全获取嵌套数据
2026-02-15 01:09:53 +08:00
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 {}
2026-02-15 01:01:03 +08:00
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')}")
2026-02-11 22:40:35 +08:00
cavity_html = f"""
2026-02-15 01:09:53 +08:00
<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>
2026-02-15 00:52:46 +08:00
</div>
2026-02-11 22:40:35 +08:00
<div class="metric">
<span class="metric-label">收缩率:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{metadata.get("shrinkage_rate", "N/A")}</span>
2026-02-11 22:40:35 +08:00
</div>
<div class="metric">
<span class="metric-label">拔模角:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{metadata.get("draft_angle", "N/A")}°</span>
2026-02-11 22:40:35 +08:00
</div>
<div class="metric">
2026-02-15 00:52:46 +08:00
<span class="metric-label">分型线长度:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{manufacturing_info.get("parting_line_length", "N/A")}</span>
2026-02-15 00:52:46 +08:00
</div>
2026-02-15 01:09:53 +08:00
<div style="margin: 10px 0; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
<strong style="color: #333;">几何特性</strong>
2026-02-11 22:40:35 +08:00
</div>
<div class="metric">
2026-02-15 00:52:46 +08:00
<span class="metric-label">产品体积:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{geo_chars.get("product_volume", "N/A")}</span>
2026-02-11 22:40:35 +08:00
</div>
<div class="metric">
<span class="metric-label">产品重量:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{geo_chars.get("product_weight", "N/A")}</span>
2026-02-15 00:52:46 +08:00
</div>
<div class="metric">
<span class="metric-label">壁厚范围:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{geo_chars.get("wall_thickness_range", "N/A")}</span>
2026-02-15 00:52:46 +08:00
</div>
2026-02-15 01:09:53 +08:00
<div style="margin: 10px 0; border-bottom: 1px solid #ddd; padding-bottom: 5px;">
<strong style="color: #333;">制造要求</strong>
2026-02-15 00:52:46 +08:00
</div>
<div class="metric">
2026-04-21 15:22:22 +08:00
<span class="metric-label">模仁材料:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{manufacturing_info.get("mold_material", "N/A")}</span>
2026-02-15 00:52:46 +08:00
</div>
<div class="metric">
<span class="metric-label">硬度:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{manufacturing_info.get("mold_hardness", "N/A")}</span>
2026-02-15 00:52:46 +08:00
</div>
<div class="metric">
<span class="metric-label">表面光洁度:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{manufacturing_info.get("surface_finish", "N/A")}</span>
2026-02-15 00:52:46 +08:00
</div>
<div class="metric">
<span class="metric-label">预估周期:</span>
2026-02-15 01:01:03 +08:00
<span class="metric-value">{manufacturing_info.get("estimated_cycle_time", "N/A")}</span>
2026-02-11 22:40:35 +08:00
</div>
</div>
"""
2026-02-15 01:01:03 +08:00
else:
2026-02-15 01:03:16 +08:00
logger.warning(f"cavity_data 为空,无法显示工艺参数 - 文件: {stp_filename}")
2026-02-15 01:09:53 +08:00
cavity_html = ""
2026-02-11 22:40:35 +08:00
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>
2026-03-07 01:09:13 +08:00
<button onclick="toggleProduct()">显示/隐藏产品</button>
2026-04-21 15:22:22 +08:00
<button onclick="toggleMold()">显示/隐藏A/B板</button>
2026-03-07 01:09:13 +08:00
<button onclick="toggleParting()">显示/隐藏分型面</button>
2026-03-07 01:18:25 +08:00
<button onclick="togglePointcloud()">显示/隐藏点云</button>
2026-04-21 13:56:02 +08:00
<button id="splitBtn" onclick="splitMold()" style="background:#FF5722;color:#fff;font-weight:bold;">分模拆分</button>
2026-02-11 22:40:35 +08:00
</div>
</div>
<script>
2026-03-07 01:09:13 +08:00
// 全局变量
2026-03-07 01:28:03 +08:00
let productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh;
2026-03-07 01:09:13 +08:00
let productVisible = true;
let moldVisible = true;
let partingVisible = true;
2026-03-07 01:28:03 +08:00
let pointcloudVisible = true;
2026-03-07 01:09:13 +08:00
2026-02-11 22:40:35 +08:00
// 初始化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);
2026-03-07 01:46:38 +08:00
// 添加轨道控制器 - 必须在使用之前定义
const controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.dampingFactor = 0.25;
2026-03-07 01:18:25 +08:00
// 创建几何体(使用点云数据或模拟模具形状)
2026-02-11 22:40:35 +08:00
const geometryData = {json.dumps(geometry_data, indent=2)};
2026-03-07 01:09:13 +08:00
const cavityData = {json.dumps(cavity_data, indent=2) if cavity_data else 'null'};
2026-03-07 01:18:25 +08:00
const pointcloudData = {json.dumps(pointcloud_data, indent=2) if pointcloud_data else 'null'};
2026-02-11 22:40:35 +08:00
2026-04-23 23:57:34 +08:00
function toFlatArray(data) {{
if (!Array.isArray(data)) return [];
if (data.length === 0) return [];
2026-05-02 00:39:04 +08:00
return Array.isArray(data[0]) ? data.flat(Infinity) : data;
2026-04-23 23:57:34 +08:00
}}
function normalizePositions(rawPositions, center) {{
const flat = toFlatArray(rawPositions);
if (!flat.length) return [];
const normalized = [];
for (let i = 0; i + 2 < flat.length; i += 3) {{
const x = Number(flat[i]);
const y = Number(flat[i + 1]);
const z = Number(flat[i + 2]);
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {{
normalized.push(
x - Number(center[0] || 0),
y - Number(center[1] || 0),
z - Number(center[2] || 0)
);
}}
}}
return normalized;
}}
function fitCameraToScene() {{
2026-05-02 00:39:04 +08:00
const objects = [productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh].filter(Boolean);
if (!objects.length) return;
const sceneBox = new THREE.Box3();
objects.forEach(obj => sceneBox.expandByObject(obj));
2026-04-23 23:57:34 +08:00
if (sceneBox.isEmpty()) return;
const center = new THREE.Vector3();
const size = new THREE.Vector3();
sceneBox.getCenter(center);
sceneBox.getSize(size);
const maxDim = Math.max(size.x, size.y, size.z) || 100;
2026-05-02 00:39:04 +08:00
const distance = Math.max(maxDim * 1.8, 30);
camera.near = Math.max(maxDim / 2000, 0.01);
camera.far = Math.max(maxDim * 200, 5000);
camera.updateProjectionMatrix();
2026-04-23 23:57:34 +08:00
camera.position.set(center.x + distance, center.y + distance, center.z + distance);
controls.target.copy(center);
2026-05-02 00:39:04 +08:00
controls.minDistance = Math.max(maxDim * 0.03, 0.5);
controls.maxDistance = Math.max(maxDim * 30, 2000);
2026-04-23 23:57:34 +08:00
controls.update();
}}
2026-05-02 00:39:04 +08:00
function registerInitialPose(mesh) {{
if (!mesh) return;
mesh.userData.initialPosition = mesh.position.clone();
mesh.userData.initialVisible = mesh.visible;
}}
function computeBounds(rawPositionsList) {{
let minX = Infinity, minY = Infinity, minZ = Infinity;
let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
let hasPoint = false;
for (const raw of rawPositionsList) {{
const flat = toFlatArray(raw);
for (let i = 0; i + 2 < flat.length; i += 3) {{
const x = Number(flat[i]);
const y = Number(flat[i + 1]);
const z = Number(flat[i + 2]);
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) continue;
hasPoint = true;
minX = Math.min(minX, x); minY = Math.min(minY, y); minZ = Math.min(minZ, z);
maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); maxZ = Math.max(maxZ, z);
}}
}}
if (!hasPoint) return null;
return {{
center: [(minX + maxX) / 2, (minY + maxY) / 2, (minZ + maxZ) / 2],
dimensions: [Math.max(maxX - minX, 1), Math.max(maxY - minY, 1), Math.max(maxZ - minZ, 1)],
}};
}}
function isValidIndexedGeometry(positions, indices) {{
if (!positions || !indices) return false;
if (positions.length < 9 || indices.length < 3) return false;
if (positions.length % 3 !== 0 || indices.length % 3 !== 0) return false;
const vertexCount = positions.length / 3;
for (let i = 0; i < indices.length; i++) {{
const idx = Number(indices[i]);
if (!Number.isFinite(idx) || idx < 0 || idx >= vertexCount) return false;
}}
return true;
}}
2026-03-07 01:18:25 +08:00
// 根据边界框创建几何体
2026-05-02 00:39:04 +08:00
const fallbackBBox = computeBounds([
pointcloudData?.points,
cavityData?.mold_cavities?.cavity?.vertices,
cavityData?.mold_cavities?.core?.vertices
]);
const bbox = geometryData.bounding_box || fallbackBBox || {{
center: [0, 0, 0],
dimensions: [100, 100, 100]
}};
2026-02-11 22:40:35 +08:00
if (bbox) {{
2026-04-23 23:57:34 +08:00
const centerOffset = bbox.center || [0, 0, 0];
2026-02-11 22:40:35 +08:00
const width = bbox.dimensions ? bbox.dimensions[0] : 100;
const height = bbox.dimensions ? bbox.dimensions[1] : 100;
const depth = bbox.dimensions ? bbox.dimensions[2] : 100;
2026-05-02 00:39:04 +08:00
const coreRequired = cavityData?.metadata?.core_required !== false;
2026-02-11 22:40:35 +08:00
2026-03-07 01:18:25 +08:00
// 如果有点云数据,创建点云模型
if (pointcloudData && pointcloudData.points && pointcloudData.points.length > 0) {{
2026-05-02 00:39:04 +08:00
let productRendered = false;
// 优先用真实三角网格渲染产品
if (pointcloudData.vertices && pointcloudData.faces) {{
const productVerts = normalizePositions(pointcloudData.vertices, centerOffset);
const productFaces = toFlatArray(pointcloudData.faces);
if (productVerts.length >= 9 && productFaces.length >= 3) {{
const productGeometry = new THREE.BufferGeometry();
productGeometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(productVerts), 3));
productGeometry.setIndex(new THREE.BufferAttribute(new Uint32Array(productFaces), 1));
productGeometry.computeVertexNormals();
const productMaterial = new THREE.MeshPhongMaterial({{
color: 0x4CAF50,
transparent: true,
opacity: 0.45,
side: THREE.DoubleSide
}});
productMesh = new THREE.Mesh(productGeometry, productMaterial);
scene.add(productMesh);
registerInitialPose(productMesh);
productRendered = true;
}}
2026-03-07 01:18:25 +08:00
}}
2026-05-02 00:39:04 +08:00
// 网格不可用时回退点云
if (!productRendered) {{
const pointPositions = normalizePositions(pointcloudData.points, centerOffset);
if (pointPositions.length >= 3) {{
const pointGeometry = new THREE.BufferGeometry();
const positions = new Float32Array(pointPositions);
pointGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
if (pointcloudData.normals && pointcloudData.normals.length > 0) {{
const normals = new Float32Array(toFlatArray(pointcloudData.normals));
if (normals.length === positions.length) {{
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);
registerInitialPose(pointcloudMesh);
}}
}}
2026-03-07 01:18:25 +08:00
}} 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);
2026-05-02 00:39:04 +08:00
registerInitialPose(productMesh);
2026-03-07 01:18:25 +08:00
}}
2026-03-07 01:09:13 +08:00
2026-04-21 15:22:22 +08:00
// 创建A板/定模(蓝色,分型面以上)
2026-03-08 02:14:07 +08:00
// 尝试从后端数据获取实际几何,否则使用简化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();
2026-04-23 23:57:34 +08:00
const positions = new Float32Array(normalizePositions(cavityVerts, centerOffset));
const indices = new Uint32Array(toFlatArray(cavityFaces));
2026-05-02 00:39:04 +08:00
let cavityBuilt = false;
if (isValidIndexedGeometry(positions, indices)) {{
cavityGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
cavityGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
cavityGeometry.computeVertexNormals();
cavityBuilt = true;
}}
if (cavityBuilt) {{
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);
registerInitialPose(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);
}} else {{
createSimpleCavity(width, height, depth);
}}
2026-03-08 02:14:07 +08:00
}} else {{
createSimpleCavity(width, height, depth);
}}
}} else {{
createSimpleCavity(width, height, depth);
}}
2026-02-11 22:40:35 +08:00
2026-04-21 15:22:22 +08:00
// 创建B板/动模(橙色,分型面以下)
2026-05-02 00:39:04 +08:00
if (coreRequired && cavityData && cavityData.mold_cavities && cavityData.mold_cavities.core && cavityData.mold_cavities.core.vertices) {{
2026-03-08 02:14:07 +08:00
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();
2026-04-23 23:57:34 +08:00
const positions = new Float32Array(normalizePositions(coreVerts, centerOffset));
const indices = new Uint32Array(toFlatArray(coreFaces));
2026-05-02 00:39:04 +08:00
let coreBuilt = false;
if (isValidIndexedGeometry(positions, indices)) {{
coreGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
coreGeometry.setIndex(new THREE.BufferAttribute(indices, 1));
coreGeometry.computeVertexNormals();
coreBuilt = true;
}}
if (coreBuilt) {{
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);
registerInitialPose(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);
}}
2026-03-08 02:14:07 +08:00
}} else {{
createSimpleCore(width, height, depth);
}}
2026-05-02 00:39:04 +08:00
}} else if (coreRequired) {{
2026-03-08 02:14:07 +08:00
createSimpleCore(width, height, depth);
}}
2026-03-07 01:09:13 +08:00
// 创建分型面(红色平面)
2026-04-23 23:57:34 +08:00
const partingGeometry = new THREE.PlaneGeometry(width * 1.2, height * 1.2);
2026-03-07 01:09:13 +08:00
const partingMaterial = new THREE.MeshBasicMaterial({{
color: 0xF44336,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide
}});
2026-03-07 01:18:25 +08:00
partingMesh = new THREE.Mesh(partingGeometry, partingMaterial);
2026-03-07 01:09:13 +08:00
partingMesh.position.set(0, 0, 0);
scene.add(partingMesh);
2026-05-02 00:39:04 +08:00
registerInitialPose(partingMesh);
2026-03-07 01:09:13 +08:00
2026-03-08 02:14:07 +08:00
// 添加产品线框
2026-03-07 01:18:25 +08:00
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);
}}
2026-03-08 02:14:07 +08:00
}}
2026-04-21 15:22:22 +08:00
// 创建简化型腔/A板(定模,分型面以上)— 备用
2026-03-08 02:14:07 +08:00
function createSimpleCavity(width, height, depth) {{
2026-04-21 15:22:22 +08:00
// A板:分型面(Z中心)以上的上半模
2026-04-23 23:57:34 +08:00
const halfDepth = depth / 2;
const cavityGeometry = new THREE.BoxGeometry(width * 1.2, height * 1.2, halfDepth + 10);
2026-03-08 02:14:07 +08:00
const cavityMaterial = new THREE.MeshPhongMaterial({{
color: 0x2196F3,
transparent: true,
opacity: 0.4,
wireframe: false
}});
cavityMesh = new THREE.Mesh(cavityGeometry, cavityMaterial);
2026-04-21 15:22:22 +08:00
// Z轴开模:A板放在分型面以上
2026-04-23 23:57:34 +08:00
cavityMesh.position.set(0, 0, halfDepth / 2 + 5);
2026-03-08 02:14:07 +08:00
scene.add(cavityMesh);
2026-05-02 00:39:04 +08:00
registerInitialPose(cavityMesh);
2026-03-07 01:09:13 +08:00
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);
2026-03-08 02:14:07 +08:00
}}
2026-04-21 15:22:22 +08:00
// 创建简化型芯/B板(动模,分型面以下)— 备用
2026-03-08 02:14:07 +08:00
function createSimpleCore(width, height, depth) {{
2026-04-21 15:22:22 +08:00
// B板:分型面(Z中心)以下的下半模
2026-04-23 23:57:34 +08:00
const halfDepth = depth / 2;
const coreGeometry = new THREE.BoxGeometry(width * 1.2, height * 1.2, halfDepth + 10);
2026-03-08 02:14:07 +08:00
const coreMaterial = new THREE.MeshPhongMaterial({{
color: 0xFF9800,
transparent: true,
opacity: 0.4,
wireframe: false
}});
coreMesh = new THREE.Mesh(coreGeometry, coreMaterial);
2026-04-21 15:22:22 +08:00
// Z轴开模:B板放在分型面以下
2026-04-23 23:57:34 +08:00
coreMesh.position.set(0, 0, -halfDepth / 2 - 5);
2026-03-08 02:14:07 +08:00
scene.add(coreMesh);
2026-05-02 00:39:04 +08:00
registerInitialPose(coreMesh);
2026-03-07 01:09:13 +08:00
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);
2026-02-11 22:40:35 +08:00
}}
2026-04-23 23:57:34 +08:00
// 统一按场景包围盒调整相机,避免模型错位/尺度不一致导致观感混乱
fitCameraToScene();
2026-02-11 22:40:35 +08:00
// 动画循环
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() {{
2026-05-02 00:39:04 +08:00
if (splitAnimId) {{
cancelAnimationFrame(splitAnimId);
splitAnimId = null;
}}
isSplit = false;
const btn = document.getElementById('splitBtn');
if (btn) btn.textContent = '分模拆分';
[productMesh, cavityMesh, coreMesh, partingMesh, pointcloudMesh].forEach(mesh => {{
if (!mesh) return;
if (mesh.userData.initialPosition) {{
mesh.position.copy(mesh.userData.initialPosition);
}} else {{
mesh.position.set(0, 0, 0);
}}
mesh.visible = mesh.userData.initialVisible !== false;
}});
if (partingMesh && partingMesh.material) {{
partingMesh.material.opacity = 0.3;
partingMesh.visible = true;
}}
fitCameraToScene();
2026-02-11 22:40:35 +08:00
}}
function toggleWireframe() {{
scene.traverse((child) => {{
if (child.isMesh) {{
child.material.wireframe = !child.material.wireframe;
}}
}});
}}
2026-03-07 01:09:13 +08:00
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;
}}
}}
2026-03-07 01:18:25 +08:00
function togglePointcloud() {{
if (pointcloudMesh) {{
pointcloudVisible = !pointcloudVisible;
pointcloudMesh.visible = pointcloudVisible;
2026-03-07 01:23:42 +08:00
}}
2026-03-07 01:18:25 +08:00
}}
2026-04-21 13:56:02 +08:00
// ─── 分模拆分动画 ───
let isSplit = false;
let splitAnimId = null;
2026-04-21 14:52:37 +08:00
// 根据材料类型确定分模方向
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';
}}
2026-04-21 13:56:02 +08:00
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];
2026-04-21 14:52:37 +08:00
const dir = getPartingDirection();
2026-04-21 13:56:02 +08:00
2026-04-21 14:52:37 +08:00
// 根据分模方向确定拆分轴和距离
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';
}}
2026-04-21 13:56:02 +08:00
// 分型面动画目标透明度
const partingTargetOpacity = isSplit ? 0 : 0.3;
// 记录起始位置
2026-04-21 14:52:37 +08:00
const cavityStart = cavityMesh ? cavityMesh.position[axis] : 0;
const coreStart = coreMesh ? coreMesh.position[axis] : 0;
2026-04-21 13:56:02 +08:00
const partingStartOpacity = partingMesh ? partingMesh.material.opacity : 0.3;
2026-04-21 14:52:37 +08:00
// 目标位置:型腔正向移动,型芯负向移动
const cavityTarget = isSplit ? splitDist : 0;
const coreTarget = isSplit ? -splitDist : 0;
2026-04-21 13:56:02 +08:00
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) {{
2026-04-21 14:52:37 +08:00
cavityMesh.position[axis] = cavityStart + (cavityTarget - cavityStart) * ease;
2026-04-21 13:56:02 +08:00
}}
if (coreMesh) {{
2026-04-21 14:52:37 +08:00
coreMesh.position[axis] = coreStart + (coreTarget - coreStart) * ease;
2026-04-21 13:56:02 +08:00
}}
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);
}}
2026-02-11 22:40:35 +08:00
</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(
2026-02-15 00:54:58 +08:00
self,
geometry_data: Dict[str, Any],
stp_filename: str,
2026-03-07 01:18:25 +08:00
cavity_data: Optional[Dict[str, Any]] = None,
2026-04-23 23:37:39 +08:00
pointcloud_data: Optional[Dict[str, Any]] = None,
suffix: Optional[str] = None,
2026-02-11 22:40:35 +08:00
) -> str:
"""生成并保存可视化HTML文件"""
try:
# 生成HTML内容
2026-02-15 00:54:58 +08:00
html_content = self.generate_3d_viewer_html(
geometry_data,
stp_filename,
2026-03-07 01:18:25 +08:00
cavity_data=cavity_data,
pointcloud_data=pointcloud_data
2026-02-15 00:54:58 +08:00
)
2026-02-11 22:40:35 +08:00
# 创建文件名
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
safe_filename = stp_filename.replace('.', '_').replace(' ', '_')
2026-04-23 23:37:39 +08:00
suffix_part = f"_{suffix}" if suffix else ""
html_filename = f"{safe_filename}{suffix_part}_{timestamp}.html"
2026-02-15 00:54:58 +08:00
2026-02-11 22:40:35 +08:00
# 保存文件
file_path = self.save_html_file(html_content, html_filename)
2026-02-15 00:54:58 +08:00
2026-02-11 22:40:35 +08:00
return file_path
2026-02-15 00:54:58 +08:00
2026-02-11 22:40:35 +08:00
except Exception as e:
logger.error(f"生成可视化文件失败: {e}")
raise