Files
Atomizer/optimization_engine/nx/mesh_converter.py

134 lines
4.4 KiB
Python
Raw Normal View History

"""
Mesh Converter Utility
Converts Nastran BDF/OP2 files to GLTF for web visualization
"""
import json
import numpy as np
from pathlib import Path
from typing import Optional, Dict, Any
import trimesh
from pyNastran.bdf.bdf import BDF
from pyNastran.op2.op2 import OP2
def convert_study_mesh(study_dir: Path) -> Optional[Path]:
"""
Convert the mesh and results of a study to GLTF format.
Args:
study_dir: Path to the study directory
Returns:
Path to the generated GLTF file, or None if conversion failed
"""
try:
# Locate files
setup_dir = study_dir / "1_setup" / "model"
results_dir = study_dir / "2_results"
vis_dir = study_dir / "3_visualization"
vis_dir.mkdir(parents=True, exist_ok=True)
# Find BDF/DAT file
bdf_files = list(setup_dir.glob("*.dat")) + list(setup_dir.glob("*.bdf"))
if not bdf_files:
# Fallback: Generate placeholder if no BDF found
return _generate_placeholder_mesh(vis_dir)
bdf_path = bdf_files[0]
# Find OP2 file (optional)
op2_files = list(results_dir.glob("*.op2"))
op2_path = op2_files[0] if op2_files else None
# Load BDF
model = BDF()
model.read_bdf(bdf_path, xref=False)
# Extract nodes and elements
# This is a simplified extraction for shell/solid elements
# A full implementation would handle all element types
nodes = []
node_ids = []
for nid, node in model.nodes.items():
nodes.append(node.get_position())
node_ids.append(nid)
nodes = np.array(nodes)
node_map = {nid: i for i, nid in enumerate(node_ids)}
faces = []
# Process CQUAD4/CTRIA3 elements
for eid, element in model.elements.items():
if element.type == 'CQUAD4':
n = [node_map[nid] for nid in element.nodes]
faces.append([n[0], n[1], n[2]])
faces.append([n[0], n[2], n[3]])
elif element.type == 'CTRIA3':
n = [node_map[nid] for nid in element.nodes]
faces.append([n[0], n[1], n[2]])
if not faces:
# Fallback if no compatible elements found
return _generate_placeholder_mesh(vis_dir)
# Create mesh
mesh = trimesh.Trimesh(vertices=nodes, faces=faces)
# Map results if OP2 exists
if op2_path:
op2 = OP2()
op2.read_op2(op2_path)
# Example: Map displacement magnitude to vertex colors
if 1 in op2.displacements:
disp = op2.displacements[1]
# Get last timestep
t3 = disp.data[-1, :, :3] # Translation x,y,z
mag = np.linalg.norm(t3, axis=1)
# Normalize to 0-1 for coloring
if mag.max() > mag.min():
norm_mag = (mag - mag.min()) / (mag.max() - mag.min())
else:
norm_mag = np.zeros_like(mag)
# Apply colormap (simple blue-to-red)
colors = np.zeros((len(nodes), 4))
colors[:, 0] = norm_mag # R
colors[:, 2] = 1 - norm_mag # B
colors[:, 3] = 1.0 # Alpha
mesh.visual.vertex_colors = colors
# Export to GLTF
output_path = vis_dir / "model.gltf"
mesh.export(output_path)
# Save metadata
metadata = {
"node_count": len(nodes),
"element_count": len(faces),
"has_results": op2_path is not None
}
with open(vis_dir / "model.json", 'w') as f:
json.dump(metadata, f, indent=2)
return output_path
except Exception as e:
print(f"Mesh conversion error: {e}")
# Fallback on error
return _generate_placeholder_mesh(vis_dir)
def _generate_placeholder_mesh(output_dir: Path) -> Path:
"""Generate a simple box mesh for testing"""
mesh = trimesh.creation.box(extents=[10, 10, 10])
output_path = output_dir / "model.gltf"
mesh.export(output_path)
with open(output_dir / "model.json", 'w') as f:
json.dump({"placeholder": True}, f)
return output_path