ricosjp / truck

Truck is a Rust CAD Kernel.
Apache License 2.0
1.02k stars 53 forks source link

Feature Request: builder method for non-linear extrusion #58

Closed twitchyliquid64 closed 3 weeks ago

twitchyliquid64 commented 8 months ago

Hi! love your work so far!!

Consider an airfoil:

image

I can use tsweep to create a fully-symmetrical airfoil, but no such method exists in the builder module to make a non-symmetrical airfoil like shown above (where one end tapers, like a commerical jet).

Can support for such an extrusion be added?

This is what I am using right now, but I don't think its fully correct:

fn extrude_then_transform<T: Sweep<Point3, Curve, Surface>>(
    elem: &T,
    extrude: Vector3,
    transform: Matrix4,
) -> T::Swept {
    let trsl_ex = Matrix4::from_translation(extrude);
    elem.sweep(
        &move |pt| transform.transform_point(trsl_ex.transform_point(*pt)),
        &move |curve| curve.transformed(trsl_ex).transformed(transform),
        &move |surface| surface.transformed(trsl_ex).transformed(transform),
        &move |pt0, pt1| Curve::Line(Line(*pt0, *pt1)),
        &move |curve0, curve1| match (curve0, curve1) {
            (Curve::Line(line), Curve::Line(_)) => Surface::Plane(Plane::new(
                line.0,
                line.1,
                transform.transform_point(line.1) + extrude,
            )),
            _ => unreachable!(),
        },
    )
}

So far: https://github.com/twitchyliquid64/airfoil-to-stl

Markk116 commented 1 month ago

I think this feature would be very nice!

Out of curiosity, for this application why not just create a shell using builder::try_wire_homotopy between the root and the wing tip?

twitchyliquid64 commented 1 month ago

Out of curiosity, for this application why not just create a shell using builder::try_wire_homotopy between the root and the wing tip?

Wait that works? I thought try_wire_homotopy was for closing a path into a face in 2d.

Markk116 commented 1 month ago

No, I think that is truck_modeling::builder::homotopy, truck_modeling::builder::try_wire_homotopy is different:

https://docs.rs/truck-modeling/latest/truck_modeling/builder/fn.try_wire_homotopy.html

Since I will need this functionality for a project anyway, I have written some toy code to illustrate how to use it to make a wing. For the 'proper' version I would interpolate the airfoil points into a bezier spline. Forgive all the .unwrap(), this is just a concept:

use truck_modeling::*;
use truck_modeling::cgmath::{Deg, Matrix4, Quaternion, Vector3};
use truck_stepio::out;
use truck_topology::compress::CompressedSolid;

fn main() {
    let outline = parse_airfoil(AIRFOIL);
    let new_wing = make_wing(outline, 0.5, 4.0, 1.2, 0.3, Deg(0.0), Deg(10.0));

    // Compress the wing solid
    let compressed: CompressedSolid<Point3, Curve, Surface> = new_wing.compress();

    // Prepare the STEP file string
    let step_string = out::CompleteStepDisplay::new(
        out::StepModel::from(&compressed),
        out::StepHeaderDescriptor {
            origination_system: "wing_new".to_owned(),
            ..Default::default()
        },
    )
    .to_string();

    // Write the STEP file
    let output_step_file = "wing_new.step";
    let mut step_file = std::fs::File::create(output_step_file).unwrap();
    std::io::Write::write_all(&mut step_file, step_string.as_ref()).unwrap();
    println!("wing model exported to {}", output_step_file);
}

// Function to apply twist to the airfoil at a given span position
fn apply_twist(airfoil: &Wire, twist_angle: Deg<f64>) -> Wire {
    let twist_quaternion = Quaternion::from_angle_z(Rad::from(twist_angle));
    let twist_mat = Matrix4::from(twist_quaternion);
    builder::transformed(&airfoil.clone(), twist_mat)
}

// Function to create the 3D wing solid
fn make_wing(airfoil: Wire, sweep: f64, span: f64, root_chord: f64, tip_chord: f64, root_twist: Deg<f64>, tip_twist: Deg<f64>) -> Solid {
    // Root section: Apply root twist and scale
    let mut root_airfoil = apply_twist(&airfoil, root_twist);
    let root_scale_mat = Matrix4::from_nonuniform_scale(root_chord, root_chord, 1.0);
    root_airfoil = builder::transformed(&root_airfoil, root_scale_mat);

    // Tip section: Apply tip twist, scale, and sweep
    let mut tip_airfoil = apply_twist(&airfoil, tip_twist);
    let tip_scale_mat = Matrix4::from_nonuniform_scale(tip_chord, tip_chord, 1.0);
    tip_airfoil = builder::transformed(&tip_airfoil, tip_scale_mat);
    let sweep_matrix = Matrix4::from_translation(Vector3::new(sweep, 0.0, span));
    tip_airfoil = builder::transformed(&tip_airfoil, sweep_matrix);

    // Create the shell between the root and tip using `try_wire_homotopy`
    let mut shell = builder::try_wire_homotopy(&root_airfoil, &tip_airfoil).unwrap();

    // Attach closing faces to the root and tip
    shell.push(builder::try_attach_plane(&[root_airfoil]).unwrap());
    *shell.last_mut().unwrap() = shell.last().unwrap().inverse(); // Ensure correct orientation
    shell.push(builder::try_attach_plane(&[tip_airfoil]).unwrap());

    // Return the completed solid
    Solid::try_new(vec![shell]).unwrap()
}

// Function to parse an airfoil from an array of 2D points
fn parse_airfoil(points: [[f64; 2]; 90]) -> Wire {
    let mut vertices = Vec::new();
    for point in points {
        vertices.push(Vertex::new(Point3::new(point[0], point[1], 0.0)));
    }

    vertices[90/2..].reverse(); // Reverse the lower surface vertices

    let mut wire = Wire::new();
    for pair in vertices.windows(2) {
        wire.push_back(builder::line(&pair[0], &pair[1]));
    }
    wire.push_back(builder::line(&vertices.last().unwrap(), &vertices[0]));
    wire
}

// 12% JOUKOWSKI AIRFOIL (joukowsk-il) from airfoiltools.com
const AIRFOIL: [[f64; 2]; 90] = [
 [0.0000000, 0.0000000],
 [0.0012180, 0.0063290],
 [0.0048660, 0.0125800],
 [0.0109260, 0.0186800],
 [0.0193690, 0.0245530],
 [0.0301540, 0.0301310],
 [0.0432270, 0.0353490],
 [0.0585260, 0.0401490],
 [0.0759760, 0.0444790],
 [0.0954910, 0.0482940],
 [0.1169780, 0.0515580],
 [0.1403300, 0.0542450],
 [0.1654350, 0.0563370],
 [0.1921690, 0.0578250],
 [0.2204040, 0.0587090],
 [0.2500000, 0.0590000],
 [0.2808140, 0.0587170],
 [0.3126970, 0.0578860],
 [0.3454910, 0.0565430],
 [0.3790390, 0.0547300],
 [0.4131760, 0.0524950],
 [0.4477360, 0.0498910],
 [0.4825500, 0.0469750],
 [0.5174500, 0.0438060],
 [0.5522640, 0.0404480],
 [0.5868240, 0.0369610],
 [0.6209610, 0.0334080],
 [0.6545080, 0.0298470],
 [0.6873030, 0.0263360],
 [0.7191850, 0.0229270],
 [0.7500000, 0.0196670],
 [0.7795960, 0.0165980],
 [0.8078310, 0.0137550],
 [0.8345650, 0.0111680],
 [0.8596700, 0.0088550],
 [0.8830220, 0.0068300],
 [0.9045080, 0.0050990],
 [0.9240240, 0.0036570],
 [0.9414740, 0.0024960],
 [0.9567730, 0.0015970],
 [0.9698460, 0.0009370],
 [0.9806310, 0.0004850],
 [0.9890740, 0.0002060],
 [0.9951340, 0.0000620],
 [0.9987820, 0.0000080], // removed middle 2 points manually for ease
 [0.0012180, -0.0063290],
 [0.0048660, -0.0125800],
 [0.0109260, -0.0186800],
 [0.0193690, -0.0245530],
 [0.0301540, -0.0301310],
 [0.0432270, -0.0353490],
 [0.0585260, -0.0401490],
 [0.0759760, -0.0444790],
 [0.0954920, -0.0482940],
 [0.1169780, -0.0515580],
 [0.1403300, -0.0542450],
 [0.1654350, -0.0563370],
 [0.1921690, -0.0578250],
 [0.2204040, -0.0587090],
 [0.2500000, -0.0590000],
 [0.2808140, -0.0587170],
 [0.3126970, -0.0578860],
 [0.3454910, -0.0565430],
 [0.3790390, -0.0547300],
 [0.4131760, -0.0524950],
 [0.4477360, -0.0498910],
 [0.4825500, -0.0469750],
 [0.5174500, -0.0438060],
 [0.5522640, -0.0404480],
 [0.5868240, -0.0369610],
 [0.6209610, -0.0334080],
 [0.6545090, -0.0298470],
 [0.6873030, -0.0263360],
 [0.7191850, -0.0229270],
 [0.7500000, -0.0196670],
 [0.7795960, -0.0165980],
 [0.8078310, -0.0137550],
 [0.8345650, -0.0111680],
 [0.8596700, -0.0088550],
 [0.8830220, -0.0068300],
 [0.9045090, -0.0050990],
 [0.9240240, -0.0036570],
 [0.9414740, -0.0024960],
 [0.9567730, -0.0015970],
 [0.9698460, -0.0009370],
 [0.9806310, -0.0004850],
 [0.9890740, -0.0002060],
 [0.9951340, -0.0000620],
 [0.9987820, -0.0000080],
 [1.0000000, 0.0000000]
];

I hope that helps illustrate how to use try_wire_homotopy. Here's the resulting wing: image

Fly safe!

twitchyliquid64 commented 1 month ago

Wow thats incredible! try_wire_homotopy is super cool, and your solution is super cool!

One q: Why do you need to reverse the bottom surface vertex? vertices[90/2..].reverse(); // Reverse the lower surface vertices

Also feel free to send a PR to https://github.com/twitchyliquid64/airfoil-to-stl with your approach, otherwise I'll try and update it myself if I have a moment!

Markk116 commented 1 month ago

Happy to hear you like it!

The thing with the bottom surface messed me up for a while, see #77. For a shell to be closed all it's normal vectors need to point in a consistent direction. So if you copy and move a face, both faces will point in the same direction. And when you perform the homotopy you get a solid where (for instance) the bottom face normal is inward, while the top face normal is facing outward.

I'm not sure if I have the time to contribute to your repo, but if I do I'll for sure make a PR!

twitchyliquid64 commented 3 weeks ago

I ended up getting around to it, thanks!

I didnt have to do anything messy with the vertices, simply inverting one of the faces was sufficient:

let mut base: Shell = builder::try_wire_homotopy(&bottom, &top).unwrap();

// Inverted bc opposite faces must have opposite normals
base.push(builder::try_attach_plane(&[bottom]).unwrap().inverse());
base.push(builder::try_attach_plane(&[top]).unwrap());

https://github.com/twitchyliquid64/airfoil-to-stl/blob/main/src/main.rs#L163-L167