import trimesh
import numpy as np
import ezdxf
from scipy.spatial import ConvexHull
from scipy.interpolate import splprep, splev
def create_layered_curves_dxf(input_file, output_file, num_layers=20, num_points_per_layer=30):
try:
yield {"status": "processing", "progress": 5, "message": "Loading and repairing mesh..."}
loaded = trimesh.load(input_file)
# 1. Geometry Extraction & Repair
if isinstance(loaded, trimesh.Scene):
meshes = [g for g in loaded.geometry.values() if isinstance(g, trimesh.Trimesh)]
if not meshes:
raise ValueError("No valid mesh geometry found in scene.")
mesh = trimesh.util.concatenate(meshes)
else:
mesh = loaded
# CRITICAL: Fix common STL issues that cause slicing to fail
mesh.remove_degenerate_faces()
mesh.fill_holes()
if len(mesh.vertices) == 0:
raise ValueError("Mesh has no vertices after processing.")
# 2. Orientation
yield {"status": "processing", "progress": 15, "message": "Optimizing orientation..."}
extents = mesh.extents
longest_axis_index = np.argmax(extents)
if longest_axis_index != 2:
source_vec = np.eye(3)[longest_axis_index]
transform = trimesh.geometry.align_vectors(source_vec, [0, 0, 1])
mesh.apply_transform(transform)
mesh.vertices -= mesh.center_mass
# 3. Slicing Logic with Fallbacks
bounds = mesh.bounds
z_min, z_max = bounds[:, 2]
z_height = z_max - z_min
# Use a very small absolute buffer if 1% is too large
safe_buffer = min(z_height * 0.01, 0.05)
z_levels = np.linspace(z_min + safe_buffer, z_max - safe_buffer, num_layers)
all_profiles_3d = []
yield {"status": "processing", "progress": 30, "message": "Starting slice generation..."}
for i, z in enumerate(z_levels):
# Attempt the slice
section = mesh.section(plane_origin=[0, 0, z], plane_normal=[0, 0, 1])
if section is None:
continue
# Convert 3D section to 2D
try:
# Use discrete paths to handle multi-part slices
paths = section.to_planar()[0].discrete
if not paths:
continue
slice_points = np.vstack(paths)
if len(slice_points) < 3:
continue
# Generate a simplified boundary via Convex Hull
hull = ConvexHull(slice_points)
hull_pts = slice_points[hull.vertices]
# Interpolate to get smooth, standardized point counts
# splprep needs unique points, so we add a tiny bit of noise if needed
tck, u = splprep([hull_pts[:, 0], hull_pts[:, 1]], s=0, per=True)
u_new = np.linspace(0, 1, num_points_per_layer)
x_new, y_new = splev(u_new, tck)
# Standardize orientation (start at max X)
pts_2d = np.column_stack((x_new, y_new))
start_idx = np.argmax(pts_2d[:, 0])
pts_2d = np.roll(pts_2d, -start_idx, axis=0)
# Back to 3D
profile_3d = np.column_stack((pts_2d, np.full(len(pts_2d), z)))
all_profiles_3d.append(profile_3d)
except Exception:
continue # Skip failing layers silently to allow completion
yield {"status": "processing", "progress": 30 + int((i/num_layers)*50)}
if not all_profiles_3d:
# LAST RESORT: Try one slice at the exact center if all else failed
center_z = (z_min + z_max) / 2
# ... (Simplified single-slice logic could go here)
raise ValueError(f"Failed to intersect model at any of the {num_layers} levels. Is the model empty?")
# 4. Export
yield {"status": "processing", "progress": 90, "message": "Writing DXF..."}
doc = ezdxf.new()
msp = doc.modelspace()
for poly in all_profiles_3d:
msp.add_polyline3d(poly, close=True)
doc.saveas(output_file)
yield {"status": "complete", "progress": 100}
except Exception as e:
yield {"status": "error", "message": str(e)}