Newer
Older
CNCTools / ReferenceSurfaceGenerator / backend / app / processing.py
@jerxie jerxie 25 days ago 4 KB include the lib install
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)}