ricosjp / truck

Truck is a Rust CAD Kernel.
Apache License 2.0
986 stars 51 forks source link

Solid::try_new Returns NotClosedShell Error on Valid Manifold Shells #77

Open Markk116 opened 2 days ago

Markk116 commented 2 days ago

I think there might be an issue with the detection of closed manifolds. I'm working on implementing a function that sweeps an arbitrary profile along a linear spline, mitring the corners. However, I have been having issues with closing the shell.

I must also admit, I have been having real trouble with debugging using this crate. Maybe it is there and I have just missed it, but I cannot find a method to tell me the normal vector of a face to check its orientation properly. As a stopgap I have tried inverting the tube and the faces in all possible combinatorial options, but to no avail.

So after a while, I decided to try Solid::new_uncheckecked and that works. Now when I open the resulting step file in Prusaslicer it seems like there are actually no errors in the BREP. So that is why I think there is an issue with the Solid::new method.

I have attached a minimal code example, which is a slimmed-down version of my project code:

use truck_modeling::builder::*;
use truck_modeling::*;
use truck_modeling::{cgmath::Quaternion, InnerSpace, Matrix4};
use truck_modeling::{Point3, Solid, Vector3, Wire};
use truck_topology::compress::CompressedSolid;
use truck_stepio::out::{CompleteStepDisplay, StepHeaderDescriptor, StepModel};
use std::fs::File;
use std::io::Write;

fn main() -> Result<(), > {
    // Define the skeleton path
    let skeleton = vec![
        Point3::new(0.0, 0.0, 0.0),
        Point3::new(0.0, 10.0, 0.0),
        Point3::new(10.0, 10.0, 0.0),
        Point3::new(20.0, 20.0, 0.0),
    ];

    // Create a triangular profile wire
    let profile = create_triangular_profile();

    // Attach the profile to create the starting face
    let face = try_attach_plane(&[profile.clone()])?;
    let mut faces = vec![face];

    // Generate frames by sweeping the profile along the skeleton
    let frames = sweep_along_skeleton(&skeleton, &profile);

    // Use frames to generate the side faces of the shell
    for window in frames.windows(2) {
        let side_faces = try_wire_homotopy(&window[0], &window[1])?;
        faces.extend(side_faces);
    }

    // Create an end cap face from the last frame
    let endcap_face = try_attach_plane(&[frames.last().unwrap().clone()])?;
    faces.push(endcap_face);

    let mut shell = Shell::new();
    faces.iter().for_each(|face| {shell.push(face.clone())});

    // Check shell condition
    println!("Condition: {:?}", shell.shell_condition());
    println!("connected: {:?}", shell.is_connected());
    println!("singular_vertices: {:?}", shell.singular_vertices());

    // Create and export the solid without checking
    let solid = Solid::new_unchecked(vec![shell.clone()]);
    export_solid_to_step(&solid, "solid_unchecked.step");

    // Try to create the solid (this will return Err(NotClosedShell))
    let solid = Solid::new(vec![shell.clone()]);

    // Export the solid to a STEP file
    export_solid_to_step(&solid, "solid_checked.step");

    Ok(())
}

fn create_triangular_profile() -> Wire {
    let v1 = builder::vertex(Point3::new(0.0, 0.0, 1.0));
    let v2 = builder::vertex(Point3::new(1.0, 0.0, 0.0));
    let v3 = builder::vertex(Point3::new(-1.0, 0.0, 0.0));

    let e1 = builder::line(&v1, &v2);
    let e2 = builder::line(&v2, &v3);
    let e3 = builder::line(&v3, &v1);

    let mut wire = Wire::new();
    wire.push_back(e1);
    wire.push_back(e2);
    wire.push_back(e3);

    wire
}

fn sweep_along_skeleton(skeleton: &[Point3], profile: &Wire) -> Vec<Wire> {
    let mut frames = vec![profile.clone()];
    let profile_normal = wire_normal(profile);

    // Sweep for all but the last node
    skeleton.windows(3).for_each(|window| {
        let v21 = vector_between(&window[1], &window[0]);
        let v23 = vector_between(&window[1], &window[2]);

        let v21_norm = normalize(&v21);
        let v23_norm = normalize(&v23);

        // Compute the bisector and scaling factor
        let intermediate = v21_norm + v23_norm;
        let bisector = if intermediate.magnitude2() < 1e-6 {
            Vector3::new(-v23_norm.y, v23_norm.x, 0.0)
        } else {
            normalize(&intermediate)
        };

        let scaling_factor = (1.0 / bisector.dot(Vector3::new(-v23_norm.y, v23_norm.x, 0.0))).abs();

        let up_vector = v23_norm.cross(bisector).normalize();
        let skeleton_normal = bisector.cross(up_vector).normalize();

        let angle = Rad(profile_normal.dot(skeleton_normal).acos())* -up_vector.z.signum();
        let quat = Quaternion::from_axis_angle(up_vector, angle);

        let rot_mat = Matrix4::from(quat);
        let translate_mat = Matrix4::from_translation(window[1] - skeleton[0]);
        let scale_mat = Matrix4::from_nonuniform_scale(scaling_factor, scaling_factor, 1.0);
        let transform_mat = translate_mat * rot_mat * scale_mat;

        let miter = transformed(&profile.clone(), transform_mat);

        frames.push(miter);
    });

    // Handle the last node
    let last_index = skeleton.len() - 1;
    let v_last = vector_between(&skeleton[last_index], &skeleton[last_index - 1]);
    let v_last_norm = normalize(&v_last);

    let up_vector_last = Vector3::new(v_last_norm.y, -v_last_norm.x, 0.0);
    let axis_last = up_vector_last.cross(profile_normal).normalize();
    let angle_last = Rad(profile_normal.dot(up_vector_last).acos());

    let quat_last = Quaternion::from_axis_angle(axis_last, angle_last);
    let rot_mat_last = Matrix4::from(quat_last);
    let translate_mat_last = Matrix4::from_translation(skeleton[last_index] - skeleton[0]);

    // No scaling for the last frame
    let transform_mat_last = translate_mat_last * rot_mat_last;

    let last_miter = transformed(&profile.clone(), transform_mat_last);

    frames.push(last_miter);

    frames
}

fn wire_normal(profile: &Wire) -> Vector3 {
    let (p1, p2) = profile[0].ends();
    let (_, p3) = profile[1].ends();
    let v1 = p1.get_point() - p2.get_point();
    let v2 = p3.get_point() - p2.get_point();
    v2.cross(v1).normalize()
}

fn normalize(v: &Vector3) -> Vector3 {
    v / v.magnitude()
}

fn vector_between(p1: &Point3, p2: &Point3) -> Vector3 {
    p2 - p1
}

fn export_solid_to_step(solid: &Solid, filename: &str) {
    let compressed: CompressedSolid<Point3, _, _> = solid.compress();

    let step_string = CompleteStepDisplay::new(
        StepModel::from(&compressed),
        StepHeaderDescriptor {
            origination_system: "YourProgramName".to_owned(),
            ..Default::default()
        },
    )
    .to_string();

    let mut step_file = File::create(filename).unwrap();
    step_file.write_all(step_string.as_ref()).unwrap();
}

I hope that this can help improve the crate because in general, I am very grateful for its existence, and all the hard work of the maintainers.

Thanks, Mark

p.s. if there are plans to add more sweep operations to the builder module, I would be down to help! I have done some background math for sweeping along arbitrary bezier curves already.

ytanimura commented 19 hours ago

Thank you for your issue.

the question of the title

Solid::try_new Returns NotClosedShell Error on Valid Manifold Shells

The reason why the solid is not created is because the shell is not oriented. In the truck, the term “closed shell” is used to impose "closed and oriented". By the following code, the shell was meshed and the mesh was output. It was found that the mesh on the bottom surface was facing inward.

fn output_mesh(&shell) {
    use truck_meshalgo::prelude::*;
    // meshing the shell
    let mesh = shell.triangulation(0.001).to_polygon();
    // create output obj file
    let mut shell_obj = std::fs::File::create("solid_meshed.obj").unwrap();
    // wirte the mesh to obj
    obj::write(&mesh, &mut shell_obj).unwrap();
}

スクリーンショット 2024-09-23 184105

This can be handled by inverting the faces when pushing them.

let mut faces = vec![face.inverse()];

Getting the normal vector of the face

I cannot find a method to tell me the normal vector of a face to check its orientation properly

As for the normal of the surface, once one has obtained the surface from the face, one can obtains the normal from the surface.

// get surface compatible with the face orientation
let surface = face.oriented_surface();
// get normal with parameter (0.5, 0.5)
let normal = surface.normal(0.5, 0.5);

Sweep by arbitary bezier

if there are plans to add more sweep operations to the builder module, I would be down to help! I have done some background math for sweeping along arbitrary bezier curves already.

Thank you for your kind words. There are no immediate plans, but we may contact you if necessary.