diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index e9a29a2..1ece8cf 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,7 +16,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Run Check - run: cargo check --features "parallel stl-io svg-io dxf-io truetype-text hershey-text image-io" + run: cargo check --features "parallel stl-io svg-io dxf-io truetype-text hershey-text image-io" --all-targets test: name: Test Suite diff --git a/.typos.toml b/.typos.toml new file mode 100644 index 0000000..6828b97 --- /dev/null +++ b/.typos.toml @@ -0,0 +1,15 @@ +[files] +extend-exclude = [".git", "stl/**", "hershey-fonts/*"] + +[default] +locale = "en" + +[default.extend-words] +# allow list / custom corrections +CSG = "CSG" +csg = "csg" +cgs = "CSG" +CGS = "CSG" + +iy = "iy" +toi = "toi" diff --git a/Cargo.lock b/Cargo.lock index b83c30a..118c8d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1816,9 +1816,9 @@ checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -2160,9 +2160,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -2170,9 +2170,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", diff --git a/Cargo.toml b/Cargo.toml index 5f60122..b869ea3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ rapier3d = { version = "0.24.0", optional = true } parry3d-f64 = { version = "0.19.0", optional = true } parry3d = { version = "0.19.0", optional = true } -# bevy mesh convertion +# bevy mesh conversion bevy_mesh = { version = "0.16", optional = true } bevy_asset = { version = "0.16", optional = true } wgpu-types = { version = "*", optional = true, default-features = false } # this will use the version that bevy_mesh does diff --git a/examples/A_right_triangle.rs b/examples/A_right_triangle.rs new file mode 100644 index 0000000..08ea8ef --- /dev/null +++ b/examples/A_right_triangle.rs @@ -0,0 +1,23 @@ +//! Scene A: Demonstrate a right_triangle(width=2, height=1) + +use csgrs::CSG; +use nalgebra::{Point3, Vector3}; +use std::fs; + +fn main() { + let tri_2d = CSG::right_triangle(2.0, 1.0, None); + // A tiny arrow pointing from the right-angle corner outward: + let arrow = CSG::arrow( + Point3::new(0.0, 0.0, 0.1), // at corner + Vector3::new(0.5, 0.0, 0.0), + 8, + true, + None::<()>, + ) + .scale(0.05, 0.05, 0.05); + let scene = tri_2d.extrude(0.1).union(&arrow); + let _ = fs::write( + "stl/scene_right_triangle.stl", + scene.to_stl_ascii("scene_right_triangle"), + ); +} diff --git a/examples/B_extrude_vector.rs b/examples/B_extrude_vector.rs new file mode 100644 index 0000000..eb40cd1 --- /dev/null +++ b/examples/B_extrude_vector.rs @@ -0,0 +1,15 @@ +//! Scene B: Demonstrate extrude_vector(direction) + +use csgrs::CSG; +use nalgebra::Vector3; +use std::fs; + +fn main() { + let circle2d = CSG::<()>::circle(1.0, 32, None); + // extrude along an arbitrary vector + let extruded_along_vec = circle2d.extrude_vector(Vector3::new(0.0, 0.0, 2.0)); + let _ = fs::write( + "stl/scene_extrude_vector.stl", + extruded_along_vec.to_stl_ascii("scene_extrude_vector"), + ); +} diff --git a/examples/E_center.rs b/examples/E_center.rs new file mode 100644 index 0000000..24678f0 --- /dev/null +++ b/examples/E_center.rs @@ -0,0 +1,19 @@ +//! Scene E: Demonstrate center() (moves shape so bounding box is centered on the origin) + +use csgrs::CSG; +use std::fs; + +fn main() { + let off_center_circle = CSG::<()>::circle(1.0, 32, None) + .translate(5.0, 2.0, 0.0) + .extrude(0.1); + let centered_circle = off_center_circle.center(); + let _ = fs::write( + "stl/scene_circle_off_center.stl", + off_center_circle.to_stl_ascii("scene_circle_off_center"), + ); + let _ = fs::write( + "stl/scene_circle_centered.stl", + centered_circle.to_stl_ascii("scene_circle_centered"), + ); +} diff --git a/examples/F_float.rs b/examples/F_float.rs new file mode 100644 index 0000000..63c2058 --- /dev/null +++ b/examples/F_float.rs @@ -0,0 +1,17 @@ +//! Scene F: Demonstrate float() (moves shape so bottom is at z=0) + +use csgrs::CSG; +use std::fs; + +fn main() { + let sphere_for_float = CSG::<()>::sphere(1.0, 16, 8, None).translate(0.0, 0.0, -1.5); + let floated = sphere_for_float.float(); + let _ = fs::write( + "stl/scene_sphere_before_float.stl", + sphere_for_float.to_stl_ascii("scene_sphere_before_float"), + ); + let _ = fs::write( + "stl/scene_sphere_floated.stl", + floated.to_stl_ascii("scene_sphere_floated"), + ); +} diff --git a/examples/G_inverse.rs b/examples/G_inverse.rs new file mode 100644 index 0000000..8473d6b --- /dev/null +++ b/examples/G_inverse.rs @@ -0,0 +1,16 @@ +//! Scene G: Demonstrate inverse() (flips inside/outside) + +use csgrs::CSG; +use std::fs; + +fn main() { + let sphere = CSG::<()>::sphere(1.0, 16, 8, None); + + // Hard to visualize in STL, but let's do it anyway + let inv_sphere = sphere.inverse(); + #[cfg(feature = "stl-io")] + let _ = fs::write( + "stl/scene_inverse_sphere.stl", + inv_sphere.to_stl_binary("scene_inverse_sphere").unwrap(), + ); +} diff --git a/examples/H_tessellate.rs b/examples/H_tessellate.rs new file mode 100644 index 0000000..2223f9a --- /dev/null +++ b/examples/H_tessellate.rs @@ -0,0 +1,15 @@ +//! Scene H: Demonstrate tessellate() (forces triangulation) + +use csgrs::CSG; +use std::fs; + +fn main() { + let sphere = CSG::<()>::sphere(1.0, 16, 8, None); + + let tri_sphere = sphere.tessellate(); + #[cfg(feature = "stl-io")] + let _ = fs::write( + "stl/scene_tessellate_sphere.stl", + tri_sphere.to_stl_binary("scene_tessellate_sphere").unwrap(), + ); +} diff --git a/examples/I_slice.rs b/examples/I_slice.rs new file mode 100644 index 0000000..2093fd6 --- /dev/null +++ b/examples/I_slice.rs @@ -0,0 +1,18 @@ +//! Scene I: Demonstrate slice(plane) – slice a cube at z=0 + +use csgrs::{CSG, Plane}; +use nalgebra::Vector3; +use std::fs; + +fn main() { + let cube = CSG::<()>::cube(2.0, 2.0, 2.0, None); + + let plane_z = Plane::from_normal(Vector3::z(), 0.5); + let sliced_polygons = cube.slice(plane_z); + let _ = fs::write("stl/scene_sliced_cube.stl", cube.to_stl_ascii("sliced_cube")); + // Save cross-section as well + let _ = fs::write( + "stl/scene_sliced_cube_section.stl", + sliced_polygons.to_stl_ascii("sliced_cube_section"), + ); +} diff --git a/examples/J_vertices.rs b/examples/J_vertices.rs new file mode 100644 index 0000000..c2b48a3 --- /dev/null +++ b/examples/J_vertices.rs @@ -0,0 +1,17 @@ +//! Scene J: Demonstrate re-computing vertices() or printing them + +use csgrs::CSG; +use std::fs; + +fn main() { + let circle_extruded = CSG::<()>::circle(1.0, 32, None).extrude(0.5); + let verts = circle_extruded.vertices(); + println!("Scene J circle_extruded has {} vertices", verts.len()); + // We'll still save an STL so there's a visual + let _ = fs::write( + "stl/scene_j_circle_extruded.stl", + circle_extruded + .to_stl_binary("scene_j_circle_extruded") + .unwrap(), + ); +} diff --git a/examples/K_reuleaux_polygon.rs b/examples/K_reuleaux_polygon.rs new file mode 100644 index 0000000..9c2759d --- /dev/null +++ b/examples/K_reuleaux_polygon.rs @@ -0,0 +1,13 @@ +//! Scene K: Demonstrate reuleaux_polygon with a typical triangle shape +//! (already used sides=4 in main examples, so let's do sides=3 here) + +use csgrs::CSG; +use std::fs; + +fn main() { + let reuleaux_tri = CSG::<()>::reuleaux(3, 2.0, 16, None).extrude(0.1); + let _ = fs::write( + "stl/scene_reuleaux_triangle.stl", + reuleaux_tri.to_stl_ascii("scene_reuleaux_triangle"), + ); +} diff --git a/examples/L_rotate_extrude.rs b/examples/L_rotate_extrude.rs new file mode 100644 index 0000000..d4858aa --- /dev/null +++ b/examples/L_rotate_extrude.rs @@ -0,0 +1,13 @@ +//! Scene L: Demonstrate rotate_extrude (360 deg) on a square + +use csgrs::CSG; +use std::fs; + +fn main() { + let small_square = CSG::<()>::square(1.0, 1.0, None).translate(2.0, 0.0, 0.0); + let revolve = small_square.rotate_extrude(360.0, 24); + let _ = fs::write( + "stl/scene_square_revolve_360.stl", + revolve.to_stl_ascii("scene_square_revolve_360"), + ); +} diff --git a/examples/M_mirror.rs b/examples/M_mirror.rs new file mode 100644 index 0000000..b06d64b --- /dev/null +++ b/examples/M_mirror.rs @@ -0,0 +1,17 @@ +//! Scene M: Demonstrate “mirror” across a Y=0 plane + +use csgrs::{CSG, Plane}; +use nalgebra::Vector3; +use std::fs; + +fn main() { + let plane_y = Plane::from_normal(Vector3::y(), 0.0); + let shape = CSG::<()>::square(2.0, 1.0, None) + .translate(1.0, 1.0, 0.0) + .extrude(0.1); + let mirrored = shape.mirror(plane_y); + let _ = fs::write( + "stl/scene_square_mirrored_y.stl", + mirrored.to_stl_ascii("scene_square_mirrored_y"), + ); +} diff --git a/examples/N_scale.rs b/examples/N_scale.rs new file mode 100644 index 0000000..156607c --- /dev/null +++ b/examples/N_scale.rs @@ -0,0 +1,15 @@ +//! Scene N: Demonstrate scale() + +use csgrs::csg::CSG; +use std::fs; + +fn main() { + let sphere = CSG::<()>::sphere(1.0, 16, 8, None); + + let scaled = sphere.scale(1.0, 2.0, 0.5); + #[cfg(feature = "stl-io")] + let _ = fs::write( + "stl/scene_scaled_sphere.stl", + scaled.to_stl_binary("scene_scaled_sphere").unwrap(), + ); +} diff --git a/examples/O_transform.rs b/examples/O_transform.rs new file mode 100644 index 0000000..feaae3f --- /dev/null +++ b/examples/O_transform.rs @@ -0,0 +1,18 @@ +//! Scene O: Demonstrate transform() with an arbitrary affine matrix + +use csgrs::csg::CSG; +use std::fs; + +fn main() { + use nalgebra::{Matrix4, Translation3}; + let xlate = Translation3::new(2.0, 0.0, 1.0).to_homogeneous(); + // Scale matrix + let scale_mat = Matrix4::new_scaling(0.5); + // Combine + let transform_mat = xlate * scale_mat; + let shape = CSG::<()>::cube(1.0, 1.0, 1.0, None).transform(&transform_mat); + let _ = fs::write( + "stl/scene_transform_cube.stl", + shape.to_stl_ascii("scene_transform_cube"), + ); +} diff --git a/examples/P_offset.rs b/examples/P_offset.rs new file mode 100644 index 0000000..68a7b77 --- /dev/null +++ b/examples/P_offset.rs @@ -0,0 +1,14 @@ +//! Scene P: Demonstrate offset(distance) + +use csgrs::CSG; +use std::fs; + +fn main() { + let poly_2d = CSG::<()>::polygon(&[[0.0, 0.0], [2.0, 0.0], [1.0, 1.5]], None); + let grown = poly_2d.offset(0.2); + let scene = grown.extrude(0.1); + let _ = fs::write( + "stl/scene_offset_grown.stl", + scene.to_stl_ascii("scene_offset_grown"), + ); +} diff --git a/examples/airfoil.rs b/examples/airfoil.rs new file mode 100644 index 0000000..182fd0a --- /dev/null +++ b/examples/airfoil.rs @@ -0,0 +1,31 @@ +//! This example demos creating airfoils with naca2412 and naca0015 profiles + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/airfoil"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 2-D profile for NACA 2412, 1 m chord, 100 pts / surface + let naca2412 = CSG::airfoil("2412", 1.0, 100, None); + write_example(&naca2412, "naca2412"); + + // quick solid wing rib 5 mm thick + let rib = naca2412.extrude(0.005); + write_example(&rib, "naca2412_extruded"); + + // symmetric foil for a centerboard + let naca0015 = CSG::airfoil("0015", 0.3, 80, None) + .extrude_vector(nalgebra::Vector3::new(0.0, 0.0, 1.2)); + write_example(&naca0015, "naca0015"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/arrow.rs b/examples/arrow.rs new file mode 100644 index 0000000..41027c6 --- /dev/null +++ b/examples/arrow.rs @@ -0,0 +1,34 @@ +//! This example demos creating an arrow and an inverse of the same arrow + +use csgrs::CSG; +use nalgebra::{Point3, Vector3}; +use std::{fs, path::Path}; + +const PATH: &str = "stl/arrow"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // start point + let start = Point3::new(1.0, 1.0, 1.0); + // Arrow direction vector, the arrow’s length is the norm of the direction vector. + let direction = Vector3::new(10.0, 5.0, 20.0); + + // number of segments for the cylindrical shaft and head + let segments = 16; + + // Create the arrow. We pass `None` for metadata. + let arrow = CSG::arrow(start, direction, segments, true, None::<()>); + write_example(&arrow, "arrow"); + + let arrow_reversed = CSG::arrow(start, direction, segments, false, None::<()>); + write_example(&arrow_reversed, "arrow_reversed"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/b-spline.rs b/examples/b-spline.rs new file mode 100644 index 0000000..5b879cf --- /dev/null +++ b/examples/b-spline.rs @@ -0,0 +1,35 @@ +//! This example shows computing a uniform B-spline + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/b-spline"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // --------------------------------------------------------------------- + // B-spline demo -------------------------------------------------------- + let bspline_ctrl = &[[0.0, 0.0], [1.0, 2.5], [3.0, 3.0], [5.0, 0.0], [6.0, -1.5]]; + let bspline_2d = CSG::bspline( + bspline_ctrl, + // degree p = + 3, + // seg/span + 32, + None, + ); + write_example(&bspline_2d, "bspline_2d"); + + // a quick thickening so we can see it in a solid viewer + let bspline_3d = bspline_2d.extrude(0.25); + write_example(&bspline_3d, "bspline_extruded"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/basic2d_shapes_and_offsetting.rs b/examples/basic2d_shapes_and_offsetting.rs new file mode 100644 index 0000000..bdcb219 --- /dev/null +++ b/examples/basic2d_shapes_and_offsetting.rs @@ -0,0 +1,31 @@ +//! This example shows basic 2D shapes and 2D offsetting + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/basic2d"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 7) 2D shapes and 2D offsetting + let square_2d = CSG::square(2.0, 2.0, None); // 2x2 square, centered + write_example(&square_2d, "square_2d"); + + let circle_2d = CSG::<()>::circle(1.0, 32, None); + write_example(&circle_2d, "circle_2d"); + + let grown_2d = square_2d.offset(0.5); + write_example(&grown_2d, "square_2d_grow_0_5"); + + let shrunk_2d = square_2d.offset(-0.5); + write_example(&shrunk_2d, "square_2d_shrink_0_5"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/basic_shapes.rs b/examples/basic_shapes.rs new file mode 100644 index 0000000..fa92c17 --- /dev/null +++ b/examples/basic_shapes.rs @@ -0,0 +1,28 @@ +//! This example shows basic shapes: cube, sphere, cylinder + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/basic_shapes"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 1) Basic shapes: cube, sphere, cylinder + let cube = CSG::cube(2.0, 2.0, 2.0, None); + write_example(&cube, "cube"); + + let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8, no metadata + write_example(&sphere, "sphere"); + + let cylinder = CSG::cylinder(1.0, 2.0, 32, None); // start=(0,-1,0), end=(0,1,0), radius=1.0, slices=32 + write_example(&cylinder, "cylinder"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/bevy_mesh.rs b/examples/bevy_mesh.rs new file mode 100644 index 0000000..bfea802 --- /dev/null +++ b/examples/bevy_mesh.rs @@ -0,0 +1,17 @@ +//! This example demos converting a `CSG` to a bevy mesh + +use csgrs::CSG; + +const PATH: &str = "stl/bevymesh"; + +fn main() { + #[cfg(not(feature = "bevymesh"))] + panic!("This example requires the `bevymesh` feature to be enabled"); + + #[cfg(feature = "bevymesh")] + { + let cube = CSG::<()>::cube(2.0, 2.0, 2.0, None); + + println!("{:#?}", cube.to_bevy_mesh()); + } +} diff --git a/examples/bezier_curve.rs b/examples/bezier_curve.rs new file mode 100644 index 0000000..899bfcc --- /dev/null +++ b/examples/bezier_curve.rs @@ -0,0 +1,33 @@ +//! This example shows creating a bézier curve + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/bezier_curve"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // --------------------------------------------------------------------- + // Bézier curve demo ---------------------------------------------------- + let bezier_ctrl = &[ + [0.0, 0.0], // P0 + [1.0, 2.0], // P1 + [3.0, 3.0], // P2 + [4.0, 0.0], // P3 + ]; + let bezier_2d = CSG::bezier(bezier_ctrl, 128, None); + write_example(&bezier_2d, "bezier_2d"); + + // give it a little “body” so we can see it in a solid viewer + let bezier_3d = bezier_2d.extrude(0.25); + write_example(&bezier_3d, "bezier_extruded"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/boolean_operations.rs b/examples/boolean_operations.rs new file mode 100644 index 0000000..9154a7c --- /dev/null +++ b/examples/boolean_operations.rs @@ -0,0 +1,37 @@ +//! This example shows boolean operations: Union, Subtract, Intersect + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/boolean_operations"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let cube = CSG::cube(2.0, 2.0, 2.0, None) + .translate(1.0, 0.0, 0.0) + .rotate(0.0, 45.0, 0.0) + .scale(1.0, 0.5, 2.0); + write_example(&cube, "cube"); + + let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8, no metadata + write_example(&sphere, "sphere"); + + // 3) Boolean operations: Union, Subtract, Intersect + let union_shape = cube.union(&sphere); + write_example(&union_shape, "union_cube_sphere"); + + let subtract_shape = cube.difference(&sphere); + write_example(&subtract_shape, "subtract_cube_sphere"); + + let intersect_shape = cube.intersection(&sphere); + write_example(&intersect_shape, "intersect_cube_sphere"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/circle_with_flat.rs b/examples/circle_with_flat.rs new file mode 100644 index 0000000..478a9e3 --- /dev/null +++ b/examples/circle_with_flat.rs @@ -0,0 +1,30 @@ +//! This example demos creating a circle with a flat side (looks like: 'D') and one with two flats + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/circle_with_flat"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 2. D-shape + let d_shape = CSG::circle_with_flat(5.0, 32, 2.0, None); + write_example(&d_shape, "d_shape"); + let d_3d = d_shape.extrude(1.0); + write_example(&d_3d, "d_shape_extruded"); + + // 3. Double-flat circle + let double_flat = CSG::circle_with_two_flats(8.0, 64, 3.0, None); + write_example(&double_flat, "double_flat"); + let df_3d = double_flat.extrude(0.5); + write_example(&df_3d, "double_flat_extruded"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/circle_with_keyway.rs b/examples/circle_with_keyway.rs new file mode 100644 index 0000000..fa7b07c --- /dev/null +++ b/examples/circle_with_keyway.rs @@ -0,0 +1,26 @@ +//! This example demos creating a circle with keyway shape + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/circle_with_keyway"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 1. Circle with keyway + let keyway_shape = CSG::circle_with_keyway(10.0, 64, 2.0, 3.0, None); + write_example(&keyway_shape, "keyway_shape"); + + // Extrude it 2 units: + let keyway_3d = keyway_shape.extrude(2.0); + write_example(&keyway_3d, "keyway_3d"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/convex_hull.rs b/examples/convex_hull.rs new file mode 100644 index 0000000..26ba7ad --- /dev/null +++ b/examples/convex_hull.rs @@ -0,0 +1,33 @@ +//! This example shows Convex hull usage + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/convex_hull"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let cube = CSG::cube(2.0, 2.0, 2.0, None) + .translate(1.0, 0.0, 0.0) + .rotate(0.0, 45.0, 0.0) + .scale(1.0, 0.5, 2.0); + let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8, no metadata + + let union_shape = cube.union(&sphere); + write_example(&union_shape, "union_cube_sphere"); + + // 4) Convex hull + #[cfg(feature = "chull-io")] + let hull_of_union = union_shape.convex_hull(); + #[cfg(feature = "chull-io")] + write_example(&hull_of_union, "hull_union"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/distribute_arc.rs b/examples/distribute_arc.rs new file mode 100644 index 0000000..27442de --- /dev/null +++ b/examples/distribute_arc.rs @@ -0,0 +1,25 @@ +//! This example shows distributing a circle along an arc + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/distribute_arc"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Distribute a circle along an arc + let circle = CSG::circle(1.0, 32, None); + write_example(&circle, "circle"); + + let arc_array = circle.distribute_arc(5, 5.0, 0.0, 180.0); + write_example(&arc_array, "arc_array"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/distribute_grid.rs b/examples/distribute_grid.rs new file mode 100644 index 0000000..2ceefa5 --- /dev/null +++ b/examples/distribute_grid.rs @@ -0,0 +1,25 @@ +//! This example shows distributing a supershape along a grid + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/distribute_grid"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Create a supershape + let sshape = CSG::supershape(1.0, 1.0, 6.0, 1.0, 1.0, 1.0, 128, None); + + // Make a 4x4 grid of the supershape + let grid_of_ss = sshape.distribute_grid(4, 4, 3.0, 3.0); + write_example(&grid_of_ss, "grid_of_ss"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/distribute_linear.rs b/examples/distribute_linear.rs new file mode 100644 index 0000000..9abea43 --- /dev/null +++ b/examples/distribute_linear.rs @@ -0,0 +1,26 @@ +//! This example shows creating a `CSG` with 2d pie slice + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/distribute_linear"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Create a pie slice of radius 2, from 0 to 90 degrees + let wedge = CSG::pie_slice(2.0, 0.0, 90.0, 16, None); + + // Distribute that wedge along a linear axis + let wedge_line = wedge.distribute_linear(4, nalgebra::Vector3::new(1.0, 0.0, 0.0), 3.0); + + write_example(&wedge_line, "wedge_line"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/egg.rs b/examples/egg.rs new file mode 100644 index 0000000..15e1ffc --- /dev/null +++ b/examples/egg.rs @@ -0,0 +1,26 @@ +//! This example demos creating a 3d egg and a 2d egg outline + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/egg"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // A 3D egg shape + let egg_solid = CSG::egg(2.0, 4.0, 8, 16, None); + write_example(&egg_solid, "egg_solid_3d"); + + // 9) egg_outline(width, length, segments) [2D shape] + let egg_2d = CSG::egg_outline(2.0, 4.0, 32, None); + write_example(&egg_2d, "egg_outline_2d"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/ellipse_and_ellipsoid.rs b/examples/ellipse_and_ellipsoid.rs new file mode 100644 index 0000000..75ea0da --- /dev/null +++ b/examples/ellipse_and_ellipsoid.rs @@ -0,0 +1,26 @@ +//! This example demos creating a 3d ellipsoid and a 2d ellipse + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/ellipse_and_ellipsoid"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // An ellipsoid with X radius=2, Y radius=1, Z radius=3 + let ellipsoid = CSG::ellipsoid(2.0, 1.0, 3.0, 16, 8, None); + write_example(&ellipsoid, "ellipsoid"); + + // ellipse(width, height, segments) + let ellipse = CSG::ellipse(3.0, 1.5, 32, None); + write_example(&ellipse, "ellipse"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/extrude.rs b/examples/extrude.rs new file mode 100644 index 0000000..c64f9eb --- /dev/null +++ b/examples/extrude.rs @@ -0,0 +1,31 @@ +//! This example shows Extrude & Rotate-Extrude oparations + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/extrude"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let square_2d = CSG::square(2.0, 2.0, None); // 2x2 square, centered + let circle_2d = CSG::circle(1.0, 32, None); + + // 8) Extrude & Rotate-Extrude + let extruded_square = square_2d.extrude(1.0); + write_example(&extruded_square, "square_extrude"); + + let revolve_circle = circle_2d.translate(10.0, 0.0, 0.0).rotate_extrude(360.0, 32); + write_example(&revolve_circle, "circle_revolve_360"); + + let partial_revolve = circle_2d.translate(10.0, 0.0, 0.0).rotate_extrude(180.0, 32); + write_example(&partial_revolve, "circle_revolve_180"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/from_image.rs b/examples/from_image.rs new file mode 100644 index 0000000..50704ee --- /dev/null +++ b/examples/from_image.rs @@ -0,0 +1,40 @@ +//! This example shows creating a `CSG` from an image + +use csgrs::csg::CSG; +use image::{GrayImage, ImageBuffer}; +use std::{fs, path::Path}; + +const PATH: &str = "stl/from_image"; + +fn main() { + #[cfg(not(feature = "image"))] + compile_error!("The 'image' feature is required for this example"); + + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 15) from_image(img, threshold, closepaths, metadata) [requires "image" feature] + // Make a simple 64x64 gray image with a circle in the center + let mut img: GrayImage = ImageBuffer::new(64, 64); + + // Fill a small circle of "white" pixels in the middle + let center = (32, 32); + for y in 0..64 { + for x in 0..64 { + let dx = x as i32 - center.0; + let dy = y as i32 - center.1; + if dx * dx + dy * dy < 15 * 15 { + img.put_pixel(x, y, image::Luma([255u8])); + } + } + } + let csg_img = CSG::from_image(&img, 128, true, None).center(); + write_example(&csg_img, "gray_scale_circle"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/gears.rs b/examples/gears.rs new file mode 100644 index 0000000..fa7162a --- /dev/null +++ b/examples/gears.rs @@ -0,0 +1,83 @@ +//! This example demos creating gears, racks, and spurs + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/gears"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let gear_involute_2d = CSG::involute_gear_2d( + 2.0, // module [mm] + 20, // z – number of teeth + 20.0, // α – pressure angle [deg] + 0.05, // radial clearance + 0.02, // backlash at pitch line + 14, // segments per involute flank + None, + ); + write_example(&gear_involute_2d, "gear_involute_2d"); + + let gear_cycloid_2d = CSG::cycloidal_gear_2d( + 2.0, // module + 17, // gear teeth + 18, // mating pin-wheel teeth (zₚ = z±1) + 0.05, // clearance + 20, // segments per flank + None, + ); + write_example(&gear_cycloid_2d, "gear_cycloid_2d"); + + let rack_involute = CSG::involute_rack_2d( + 2.0, // module + 12, // number of rack teeth to generate + 20.0, // pressure angle + 0.05, // clearance + 0.02, // backlash + None, + ); + write_example(&rack_involute, "rack_involute"); + + let rack_cycloid = CSG::cycloidal_rack_2d( + 2.0, // module + 12, // teeth + 1.0, // generating-circle radius (≈ m/2 for a conventional pin-rack) + 0.05, // clearance + 24, // segments per flank + None, + ); + write_example(&rack_cycloid, "rack_cycloid"); + + let spur_involute = CSG::spur_gear_involute( + 2.0, 20, 20.0, 0.05, 0.02, 14, 12.0, // face-width (extrusion thickness) + None, + ); + write_example(&spur_involute, "spur_involute"); + + let spur_cycloid = CSG::spur_gear_cycloid( + 2.0, 17, 18, 0.05, 20, 12.0, // thickness + None, + ); + write_example(&spur_cycloid, "spur_cycloid"); + + // let helical = CSG::helical_involute_gear( + // 2.0, // module + // 20, // z + // 20.0, // pressure angle + // 0.05, 0.02, 14, + // 25.0, // face-width + // 15.0, // helix angle β [deg] + // 40, // axial slices (resolution of the twist) + // None, + // ); + // write_example(&helical, "helical_gear"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/mass_properties.rs b/examples/mass_properties.rs new file mode 100644 index 0000000..f3ee07b --- /dev/null +++ b/examples/mass_properties.rs @@ -0,0 +1,15 @@ +//! This example shows adding mass properties to a `CSG` + +use csgrs::CSG; + +const PATH: &str = "stl/mass_properties"; + +fn main() { + let cube = CSG::<()>::cube(2.0, 2.0, 2.0, None); + + // 14) Mass properties (just printing them) + let (mass, com, principal_frame) = cube.mass_properties(1.0); + println!("Cube mass = {mass}"); + println!("Cube center of mass = {:?}", com); + println!("Cube principal inertia local frame = {:?}", principal_frame); +} diff --git a/examples/metaballs.rs b/examples/metaballs.rs new file mode 100644 index 0000000..c4eb51d --- /dev/null +++ b/examples/metaballs.rs @@ -0,0 +1,58 @@ +//! This example shows creating a `CSG` from a list of metaballs + +use csgrs::{CSG, metaballs::MetaBall}; +use nalgebra::Point3; +use std::{fs, path::Path}; + +const PATH: &str = "stl/metaballs"; + +fn main() { + #[cfg(not(feature = "metaballs"))] + compile_error!( + "This example requires the metaballs feature, try adding '--features metaballs'" + ); + + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Suppose we want two overlapping metaballs + let balls = &[ + MetaBall::new(Point3::origin(), 8.0), + MetaBall::new(Point3::new(14.0, 0.0, 0.0), 8.0), + ]; + + let resolution = (65, 65, 65); + let iso_value = 1.0; + let padding = 1.5; + + let metaball_csg = CSG::metaballs(balls, resolution, iso_value, padding, None); + + // For instance, save to STL + // let stl_data = metaball_csg.to_stl_binary("my_metaballs").unwrap(); + // std::fs::write("stl/metaballs.stl", stl_data) + // .expect("Failed to write metaballs.stl"); + write_example(&metaball_csg, "two_overlapping_metaballs"); + + // Now suppose we want 4 far metaballs + let balls = &[ + MetaBall::new(Point3::origin(), 8.0), + MetaBall::new(Point3::new(20.0, 0.0, 0.0), 8.0), + MetaBall::new(Point3::new(0.0, 20.0, 0.0), 8.0), + MetaBall::new(Point3::new(-1.0, -1.0, 20.0), 8.0), + ]; + + let resolution = (72, 72, 72); + let iso_value = 1.0; + let padding = 1.9; + + let metaball_csg = CSG::metaballs(balls, resolution, iso_value, padding, None); + + write_example(&metaball_csg, "four_metaballs"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/metaballs_2d.rs b/examples/metaballs_2d.rs new file mode 100644 index 0000000..af4be73 --- /dev/null +++ b/examples/metaballs_2d.rs @@ -0,0 +1,34 @@ +//! This example shows creating a `CSG` from a list of 2d metaballs + +use csgrs::CSG; +use nalgebra::Point2; +use std::{fs, path::Path}; + +const PATH: &str = "stl/metaballs_2d"; + +fn main() { + #[cfg(not(feature = "metaballs"))] + compile_error!( + "This example requires the metaballs feature, try adding '--features metaballs'" + ); + + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Create a 2D "metaball" shape from 3 circles + let balls_2d = vec![ + (Point2::new(0.0, 0.0), 1.0), + (Point2::new(1.5, 0.0), 1.0), + (Point2::new(0.75, 1.0), 0.5), + ]; + let mb2d = CSG::metaballs2d(&balls_2d, (100, 100), 1.0, 0.25, None); + + write_example(&mb2d, "three_overlapping_2d_metaballs"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/metadata.rs b/examples/metadata.rs new file mode 100644 index 0000000..1a40341 --- /dev/null +++ b/examples/metadata.rs @@ -0,0 +1,41 @@ +//! This example shows using optional metadata for storing color and label.\ +//! Other use cases include storing speed, ID, or layer info. + +use csgrs::{Vertex, polygon::Polygon}; +use nalgebra::{Point3, Vector3}; + +/// Demo metadata type +#[derive(Clone)] +struct MyMetadata { + /// RGB-8 color of the `Polygon` + color: (u8, u8, u8), + /// Name of the `Polygon` + label: String, +} + +const PATH: &str = "stl/metadata"; + +fn main() { + // For a single polygon: + let mut poly_w_metadata = Polygon::new( + vec![ + Vertex::new(Point3::origin(), Vector3::z()), + Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()), + Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()), + ], + Some(MyMetadata { + color: (255, 0, 0), + label: "Triangle".into(), + }), + ); + + // Retrieve metadata + if let Some(data) = poly_w_metadata.metadata() { + println!("This polygon is labeled {}", data.label); + } + + // Mutate metadata + if let Some(data_mut) = poly_w_metadata.metadata_mut() { + data_mut.label.push_str("_extended"); + } +} diff --git a/examples/minkowski_sum.rs b/examples/minkowski_sum.rs new file mode 100644 index 0000000..5c33f4e --- /dev/null +++ b/examples/minkowski_sum.rs @@ -0,0 +1,30 @@ +//! This example shows computing the Minkowski sum: self ⊕ other +//! This uses a Naive approach: Take every vertex in self, add it to every vertex in other, +//! then compute the convex hull of all resulting points. + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/minkowski"; + +fn main() { + #[cfg(not(feature = "chull-io"))] + compile_error!("The 'chull-io' feature is required for this example"); + + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let cube = CSG::cube(2.0, 2.0, 2.0, None); + let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8 + + // 5) Minkowski sum + let minkowski = cube.minkowski_sum(&sphere); + write_example(&minkowski, "minkowski_cube_sphere"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/misc_2d.rs b/examples/misc_2d.rs new file mode 100644 index 0000000..5f35245 --- /dev/null +++ b/examples/misc_2d.rs @@ -0,0 +1,66 @@ +//! This example demos the following 2d shapes: +//! - polygon 2d +//! - rounded rectangle +//! - regular ngon +//! - trapezoid +//! - squircle +//! - keyhole +//! - ring +//! - heart +//! - crescent + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/misc_2d"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 1) polygon + let polygon_2d = CSG::polygon(&[[0.0, 0.0], [2.0, 0.0], [1.5, 1.0], [1.0, 2.0]], None); + let _ = fs::write("stl/polygon_2d.stl", polygon_2d.to_stl_ascii("polygon_2d")); + write_example(&polygon_2d, "polygon_2d"); + + // 2) rounded_rectangle(width, height, corner_radius, corner_segments) + let rrect_2d = CSG::rounded_rectangle(4.0, 2.0, 0.3, 8, None); + let _ = fs::write( + "stl/rounded_rectangle_2d.stl", + rrect_2d.to_stl_ascii("rounded_rectangle_2d"), + ); + write_example(&rrect_2d, "rounded_rectangle"); + + // 3) regular_ngon(sides, radius) + let ngon_2d = CSG::regular_ngon(6, 1.0, None); // Hexagon + write_example(&ngon_2d, "ngon"); + + // 4) trapezoid(top_width, bottom_width, height) + let trap_2d = CSG::trapezoid(1.0, 2.0, 2.0, 0.5, None); + write_example(&trap_2d, "trapezoid"); + + // 5) squircle(width, height, segments) + let squircle_2d = CSG::squircle(3.0, 3.0, 32, None); + write_example(&squircle_2d, "squircle"); + + // 6) keyhole(circle_radius, handle_width, handle_height, segments) + let keyhole_2d = CSG::keyhole(1.0, 1.0, 2.0, 16, None); + write_example(&keyhole_2d, "keyhole"); + + // 7) ring(inner_diam, thickness, segments) + let ring_2d = CSG::ring(5.0, 1.0, 32, None); + write_example(&ring_2d, "ring"); + + let heart2d = CSG::heart(30.0, 25.0, 128, None); + write_example(&heart2d, "heart"); + + let crescent2d = CSG::crescent(10.0, 7.0, 4.0, 64, None); + write_example(&crescent2d, "crescent"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/pie_slice.rs b/examples/pie_slice.rs new file mode 100644 index 0000000..388dff7 --- /dev/null +++ b/examples/pie_slice.rs @@ -0,0 +1,23 @@ +//! This example shows creating a `CSG` with 2d pie slice + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/pie_slice"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Create a pie slice of radius 2, from 0 to 90 degrees + let wedge = CSG::pie_slice(2.0, 0.0, 90.0, 16, None); + + write_example(&wedge, "wedge"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/polyhedron.rs b/examples/polyhedron.rs new file mode 100644 index 0000000..9bc423c --- /dev/null +++ b/examples/polyhedron.rs @@ -0,0 +1,43 @@ +//! This example shows making regular polyhedra and a custom polyhedron with a simple tetrahedron + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/polyhedron"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let oct = CSG::octahedron(10.0, None); + write_example(&oct, "octahedron"); + + // let dodec = CSG::dodecahedron(15.0, None); + // write_example(&dodec, "dodecahedron"); + + let ico = CSG::icosahedron(12.0, None); + write_example(&ico, "icosahedron"); + + // 12) Polyhedron example (simple tetrahedron): + let points = [ + [0.0, 0.0, 0.0], + [1.0, 0.0, 0.0], + [0.5, 1.0, 0.0], + [0.5, 0.5, 1.0], + ]; + let faces = &[ + vec![0, 2, 1], // base triangle + vec![0, 1, 3], // side + vec![1, 2, 3], + vec![2, 0, 3], + ]; + let poly = CSG::polyhedron(&points, faces, None); + write_example(&poly, "tetrahedron"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/ray_intersection.rs b/examples/ray_intersection.rs new file mode 100644 index 0000000..d1aea91 --- /dev/null +++ b/examples/ray_intersection.rs @@ -0,0 +1,16 @@ +//! This example shows ray intersection by printing the results) + +use csgrs::csg::CSG; +use nalgebra::{Point3, Vector3}; + +const PATH: &str = "stl/ray_intersection"; + +fn main() { + let cube: CSG<()> = CSG::cube(2.0, 2.0, 2.0, None); + + // 11) Ray intersection demo (just printing the results) + let ray_origin = Point3::new(0.0, 0.0, -5.0); + let ray_dir = Vector3::new(0.0, 0.0, 1.0); // pointing along +Z + let hits = cube.ray_intersections(&ray_origin, &ray_dir); + println!("Ray hits on the cube: {:?}", hits); +} diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000..4e9d9ce --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,18 @@ +# Examples + +Minimal example of each function of csgrs. + +These do not use any shared data, so we'll bind the generic S to ().\ +Almost all examples require the `stl-io` feature + +# Scenes + +Additional “SCENES” Demonstrating Each Function Minimally, these start with a capital letter + +In these scenes, we typically: +1) Create the shape +2) Extrude (if 2D) so we can save an STL +3) Optionally union with a small arrow that points to a location of interest in the shape +4) Save the result as an STL, e.g. "scene_XX_something.stl" + +Because many shapes are already in the main examples, these scenes are just short examples to help with explanation. diff --git a/examples/renormalize.rs b/examples/renormalize.rs new file mode 100644 index 0000000..12a22c7 --- /dev/null +++ b/examples/renormalize.rs @@ -0,0 +1,32 @@ +//! This example shows renormalizing polygons for flat shading + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/renormalize"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let cube = CSG::cube(2.0, 2.0, 2.0, None) + .translate(1.0, 0.0, 0.0) + .rotate(0.0, 45.0, 0.0) + .scale(1.0, 0.5, 2.0); + let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8 + + // polygon to renormalize + let mut union_shape = cube.union(&sphere); + write_example(&union_shape, "union_cube_sphere"); + + // 10) Renormalize polygons (flat shading): + union_shape.renormalize(); + write_example(&union_shape, "union_renormalized"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/reuleaux_polygon.rs b/examples/reuleaux_polygon.rs new file mode 100644 index 0000000..ea31e6e --- /dev/null +++ b/examples/reuleaux_polygon.rs @@ -0,0 +1,30 @@ +//! This example shows a variety reuleaux polygon usage examples + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/reuleaux_polygon"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 12) reuleaux_polygon(sides, side_len, segments) + let reuleaux3_2d = CSG::reuleaux(3, 2.0, 64, None); // Reuleaux triangle + write_example(&reuleaux3_2d, "reuleaux3_2d"); + + // 12) reuleaux_polygon(sides, radius, arc_segments_per_side) + let reuleaux4_2d = CSG::reuleaux(4, 2.0, 64, None); // Reuleaux triangle + write_example(&reuleaux4_2d, "reuleaux4_2d"); + + // 12) reuleaux_polygon(sides, radius, arc_segments_per_side) + let reuleaux5_2d = CSG::reuleaux(5, 2.0, 64, None); // Reuleaux triangle + write_example(&reuleaux5_2d, "reuleaux5_2d"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/sdf.rs b/examples/sdf.rs new file mode 100644 index 0000000..2e8000c --- /dev/null +++ b/examples/sdf.rs @@ -0,0 +1,35 @@ +//! This example shows signed distance field (sdf) usage + +use csgrs::CSG; +use nalgebra::Point3; +use std::{fs, path::Path}; + +const PATH: &str = "stl/sdf"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + #[cfg(not(feature = "sdf"))] + compile_error!("This example requires the sdf feature, try adding '--features sdf'"); + + // Example SDF for a sphere of radius 2.5 centered at (0,0,0) + let my_sdf = |p: &Point3| p.coords.norm() - 2.5; + + let resolution = (60, 60, 60); + let min_pt = Point3::new(-2.7, -2.7, -2.7); + let max_pt = Point3::new(2.7, 2.7, 2.7); + let iso_value = 0.0; // Typically zero for SDF-based surfaces + + let csg_shape = CSG::sdf(my_sdf, resolution, min_pt, max_pt, iso_value, None); + + // Now `csg_shape` is your polygon mesh as a CSG you can union, subtract, or export: + write_example(&csg_shape, "sdf_sphere"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/subdivide_triangles.rs b/examples/subdivide_triangles.rs new file mode 100644 index 0000000..5251728 --- /dev/null +++ b/examples/subdivide_triangles.rs @@ -0,0 +1,24 @@ +//! This example shows subdividing all polygons in this CSG + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/subdivide_triangles"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8, no metadata + + // 9) Subdivide triangles (for smoother sphere or shapes): + let subdiv_sphere = sphere.subdivide_triangles(2.try_into().expect("not 0")); // 2 subdivision levels + write_example(&subdiv_sphere, "sphere_subdiv2"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/supershape.rs b/examples/supershape.rs new file mode 100644 index 0000000..606329e --- /dev/null +++ b/examples/supershape.rs @@ -0,0 +1,22 @@ +//! This example shows creating a supershape + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/supershape"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // Create a supershape + let sshape = CSG::supershape(1.0, 1.0, 6.0, 1.0, 1.0, 1.0, 128, None); + write_example(&sshape, "supershape"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/teardrop.rs b/examples/teardrop.rs new file mode 100644 index 0000000..d034f66 --- /dev/null +++ b/examples/teardrop.rs @@ -0,0 +1,30 @@ +//! This example demos creating a 3d teardrop, a teardrop cylinder, and a 2d teardrop outline + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/teardrop"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // A 3D teardrop shape + let teardrop_solid = CSG::teardrop(3.0, 5.0, 32, 32, None); + write_example(&teardrop_solid, "teardrop_solid"); + + // A teardrop 'blank' hole + let teardrop_cylinder = CSG::teardrop_cylinder(2.0, 4.0, 32.0, 16, None); + write_example(&teardrop_cylinder, "teardrop_cylinder"); + + // 8) teardrop(width, height, segments) [2D shape] + let teardrop_2d = CSG::teardrop_outline(2.0, 3.0, 16, None); + write_example(&teardrop_2d, "teardrop_2d"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/text.rs b/examples/text.rs new file mode 100644 index 0000000..5e41001 --- /dev/null +++ b/examples/text.rs @@ -0,0 +1,32 @@ +//! This example shows text usage + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/text"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // 13) Text example (2D). Provide a valid TTF font data below: + // (Replace "asar.ttf" with a real full .ttf file in your project.) + let font_data = include_bytes!("../asar.ttf"); + #[cfg(feature = "truetype-text")] + let text_csg = CSG::text("HELLO", font_data, 15.0, None); + #[cfg(feature = "truetype-text")] + write_example(&text_csg, "text_hello_2d"); + + // Optionally extrude the text: + #[cfg(feature = "truetype-text")] + let text_extruded = text_csg.extrude(2.0); + #[cfg(feature = "truetype-text")] + write_example(&text_extruded, "text_hello_extruded"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/torus.rs b/examples/torus.rs new file mode 100644 index 0000000..8dde01f --- /dev/null +++ b/examples/torus.rs @@ -0,0 +1,22 @@ +//! This example demos creating a 3d torus (donut shape) + +use csgrs::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/torus"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + // torus(outer radi, inner radi, segments around the donut, segments of the tube cross-section) + let torus = CSG::torus(20.0, 5.0, 48, 24, None); + write_example(&torus, "torus_2d"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/tpms.rs b/examples/tpms.rs new file mode 100644 index 0000000..f9b6c1d --- /dev/null +++ b/examples/tpms.rs @@ -0,0 +1,46 @@ +//! This example shows Triply‑Periodic Minimal Surfaces +//! gyroid and Schwarz‑P/D shapes that use the current CSG volume as a bounding region +//! +//! Note: this requires `chull-io` + +use csgrs::csg::CSG; +use std::{fs, path::Path}; + +const PATH: &str = "stl/tpms"; + +fn main() { + // Ensure the folder exists + let _ = fs::create_dir_all(PATH); + + let cube = CSG::cube(2.0, 2.0, 2.0, None) + .translate(1.0, 0.0, 0.0) + .rotate(0.0, 45.0, 0.0) + .scale(1.0, 0.5, 2.0); + let sphere = CSG::sphere(1.0, 16, 8, None); + + let union_shape = cube.union(&sphere); + let hull_of_union = union_shape.convex_hull(); + + // ---------- Triply‑Periodic Minimal Surfaces ---------- + let gyroid_inside_cube = hull_of_union + .scale(20.0, 20.0, 20.0) + .gyroid(64, 2.0, 0.0, None); + write_example(&gyroid_inside_cube, "gyroid_cube"); + + let schwarzp_inside_cube = hull_of_union + .scale(20.0, 20.0, 20.0) + .schwarz_p(64, 2.0, 0.0, None); + write_example(&schwarzp_inside_cube, "schwarz_p_cube"); + + let schwarzd_inside_cube = hull_of_union + .scale(20.0, 20.0, 20.0) + .schwarz_d(64, 2.0, 0.0, None); + write_example(&schwarzd_inside_cube, "schwarz_d_cube"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/examples/transformations.rs b/examples/transformations.rs new file mode 100644 index 0000000..1a84273 --- /dev/null +++ b/examples/transformations.rs @@ -0,0 +1,33 @@ +//! This example shows transformations: Translate, Rotate, Scale, Mirror + +use csgrs::{CSG, plane::Plane}; +use nalgebra::Vector3; +use std::{fs, path::Path}; + +const PATH: &str = "stl/transformations"; + +fn main() { + // Ensure the /stls folder exists + let _ = fs::create_dir_all(PATH); + + let cube = CSG::cube(2.0, 2.0, 2.0, None); + write_example(&cube, "cube"); + + // 2) Transformations: Translate, Rotate, Scale, Mirror + let moved_cube = cube + .translate(1.0, 0.0, 0.0) + .rotate(0.0, 45.0, 0.0) + .scale(1.0, 0.5, 2.0); + write_example(&moved_cube, "cube_transformed"); + + let plane_x = Plane::from_normal(Vector3::x(), 0.0); + let mirrored_cube = cube.mirror(plane_x); + write_example(&mirrored_cube, "cube_mirrored_x"); +} + +fn write_example(shape: &CSG, name: &str) { + let _ = fs::write( + Path::new(PATH).join(name).with_extension("stl"), + shape.to_stl_binary(name).unwrap(), + ); +} diff --git a/readme.md b/readme.md index b8b3e1f..3a66cf7 100644 --- a/readme.md +++ b/readme.md @@ -25,7 +25,7 @@ algorithms used for triangulation only work in 2D, so **csgrs** rotates ![Example CSG output](docs/csg.png) ## Community -[![](https://dcbadge.limes.pink/api/server/https://discord.gg/9WkD3WFxMC)](https://discord.gg/9WkD3WFxMC) +[![discord logo: csgrs | member count](https://dcbadge.limes.pink/api/server/https://discord.gg/9WkD3WFxMC)](https://discord.gg/9WkD3WFxMC) ## Getting started @@ -504,7 +504,7 @@ for Polygon - determine why square_2d produces invalid output with to_stl_binary but not to_stl_ascii - bending - lead-ins, lead-outs -- gpu accelleration +- gpu acceleration - https://github.com/dimforge/wgmath - https://github.com/pcwalton/pathfinder - reduce dependency feature sets @@ -529,7 +529,7 @@ for Polygon - test geo_booleanop as alternative to geo's built-in boolean ops. - adapt cavalier_contours demo application - rethink metadata - - support storing UV[W] coordinates with vertexes at compile time (try to keep runtime cost low too) + - support storing UV[W] coordinates with vertices at compile time (try to keep runtime cost low too) - accomplish equivalence checks and memory usage reduction by using a hashmap or references instead of storing metadata with each node - with equivalence checks, returning sorted metadata becomes easy - implement half-edge, radial edge, etc to and from adapters diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..3b90d45 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,11 @@ +max_width = 95 +fn_call_width = 62 +chain_width = 62 + +use_field_init_shorthand = true +match_block_trailing_comma = true + +# unstable +reorder_impl_items = true +where_single_line = true +normalize_comments = true diff --git a/src/bsp.rs b/src/bsp.rs index 42369f9..3cd19b2 100644 --- a/src/bsp.rs +++ b/src/bsp.rs @@ -298,7 +298,7 @@ impl Node { let plane = self.plane.clone().unwrap(); // Split polygons in parallel - let (mut coplanar_front, mut coplanar_back, mut front, mut back) = polygons + let (mut coplanar_front, mut coplanar_back, front, back) = polygons .par_iter() .map(|p| plane.split_polygon(p)) // <-- just pass p .reduce( diff --git a/src/csg.rs b/src/csg.rs index e9f6de7..9581558 100644 --- a/src/csg.rs +++ b/src/csg.rs @@ -26,7 +26,7 @@ use rayon::prelude::*; /// The main CSG solid structure. Contains a list of 3D polygons, 2D polylines, and some metadata. #[derive(Debug, Clone)] -pub struct CSG { +pub struct CSG { /// 3D polygons for volumetric shapes pub polygons: Vec>, @@ -537,8 +537,8 @@ impl CSG { for poly in &mut csg.polygons { for vert in &mut poly.vertices { // Position - let hom_pos = mat * vert.pos.to_homogeneous(); - vert.pos = Point3::from_homogeneous(hom_pos).unwrap(); // todo catch error + let homog_pos = mat * vert.pos.to_homogeneous(); + vert.pos = Point3::from_homogeneous(homog_pos).unwrap(); // todo catch error // Normal vert.normal = mat_inv_transpose.transform_vector(&vert.normal).normalize(); diff --git a/src/io/svg.rs b/src/io/svg.rs index d9c0564..3347feb 100644 --- a/src/io/svg.rs +++ b/src/io/svg.rs @@ -55,7 +55,7 @@ impl PathBuilder { /// Get a mutable reference to the current path, or an error if no path has been started. /// - /// To accomodate for the semantics of [`close`], this function will automatically start a new path + /// To accommodate for the semantics of [`close`], this function will automatically start a new path /// if the last path has 2 or more points and is closed. /// For this reason, using this proxy is recommended for implementing any drawing command. fn get_path_mut_or_fail(&mut self) -> Result<&mut LineString, IoError> { @@ -377,6 +377,7 @@ impl PathBuilder { } pub trait FromSVG: Sized { + #[allow(unused)] fn from_svg(doc: &str) -> Result; } @@ -532,6 +533,7 @@ impl FromSVG for CSG<()> { } pub trait ToSVG { + #[allow(unused)] fn to_svg(&self) -> String; } @@ -879,14 +881,13 @@ fn svg_path_to_multi_line_string( /// /// [points]: https://www.w3.org/TR/SVG11/shapes.html#PointsBNF fn svg_points_to_line_string(points: &str) -> Result, IoError> { - use nom::IResult; - use nom::Parser; + use nom::{IResult, Parser}; use nom::branch::alt; use nom::character::complete::{char, multispace0, multispace1}; - use nom::combinator::opt; use nom::multi::separated_list1; use nom::number::complete::float; - use nom::sequence::{delimited, pair, preceded, separated_pair, terminated, tuple}; + use nom::sequence::{pair, tuple, delimited, separated_pair}; + use nom::combinator::opt; fn comma_wsp(i: &str) -> IResult<&str, ()> { let (i, _) = alt(( diff --git a/src/lib.rs b/src/lib.rs index 5674bc3..7b2afb4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,6 +55,8 @@ compile_error!("Either 'delaunay' or 'earcut' feature must be specified, but not compile_error!("Either 'f64' or 'f32' feature must be specified, but not both"); pub use csg::CSG; +pub use plane::Plane; +pub use polygon::Polygon; pub use vertex::Vertex; #[cfg(feature = "hashmap")] diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index a77146e..0000000 --- a/src/main.rs +++ /dev/null @@ -1,884 +0,0 @@ -// main.rs -// -// Minimal example of each function of csgrs (which is now generic over the shared-data type S). -// Here, we do not use any shared data, so we'll bind the generic S to (). - -use csgrs::float_types::Real; -use csgrs::plane::Plane; -use nalgebra::{Point3, Vector3}; -use std::fs; - -#[cfg(feature = "image")] -use image::{GrayImage, ImageBuffer}; - -#[cfg(feature = "metaballs")] -use csgrs::metaballs::MetaBall; - -// A type alias for convenience: no shared data, i.e. S = () -type CSG = csgrs::csg::CSG<()>; - -fn main() { - // Ensure the /stls folder exists - let _ = fs::create_dir_all("stl"); - - // 1) Basic shapes: cube, sphere, cylinder - let cube = CSG::cube(2.0, 2.0, 2.0, None); - #[cfg(feature = "stl-io")] - let _ = fs::write("stl/cube.stl", cube.to_stl_binary("cube").unwrap()); - - let sphere = CSG::sphere(1.0, 16, 8, None); // center=(0,0,0), radius=1, slices=16, stacks=8, no metadata - #[cfg(feature = "stl-io")] - let _ = fs::write("stl/sphere.stl", sphere.to_stl_binary("sphere").unwrap()); - - let cylinder = CSG::cylinder(1.0, 2.0, 32, None); // start=(0,-1,0), end=(0,1,0), radius=1.0, slices=32 - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/cylinder.stl", - cylinder.to_stl_binary("cylinder").unwrap(), - ); - - // 2) Transformations: Translate, Rotate, Scale, Mirror - let moved_cube = cube - .translate(1.0, 0.0, 0.0) - .rotate(0.0, 45.0, 0.0) - .scale(1.0, 0.5, 2.0); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/cube_transformed.stl", - moved_cube.to_stl_binary("cube_transformed").unwrap(), - ); - - let plane_x = Plane::from_normal(Vector3::x(), 0.0); - let mirrored_cube = cube.mirror(plane_x); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/cube_mirrored_x.stl", - mirrored_cube.to_stl_binary("cube_mirrored_x").unwrap(), - ); - - // 3) Boolean operations: Union, Subtract, Intersect - let union_shape = moved_cube.union(&sphere); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/union_cube_sphere.stl", - union_shape.to_stl_binary("union_cube_sphere").unwrap(), - ); - - let subtract_shape = moved_cube.difference(&sphere); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/subtract_cube_sphere.stl", - subtract_shape.to_stl_binary("subtract_cube_sphere").unwrap(), - ); - - let intersect_shape = cube.intersection(&sphere); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/intersect_cube_sphere.stl", - intersect_shape - .to_stl_binary("intersect_cube_sphere") - .unwrap(), - ); - - // 4) Convex hull - #[cfg(feature = "chull-io")] - let hull_of_union = union_shape.convex_hull(); - #[cfg(feature = "stl-io")] - #[cfg(feature = "chull-io")] - let _ = fs::write( - "stl/hull_union.stl", - hull_of_union.to_stl_binary("hull_union").unwrap(), - ); - - // 5) Minkowski sum - #[cfg(feature = "chull-io")] - let minkowski = cube.minkowski_sum(&sphere); - #[cfg(feature = "stl-io")] - #[cfg(feature = "chull-io")] - let _ = fs::write( - "stl/minkowski_cube_sphere.stl", - minkowski.to_stl_binary("minkowski_cube_sphere").unwrap(), - ); - - // 7) 2D shapes and 2D offsetting - let square_2d = CSG::square(2.0, 2.0, None); // 2x2 square, centered - let _ = fs::write("stl/square_2d.stl", square_2d.to_stl_ascii("square_2d")); - - let circle_2d = CSG::circle(1.0, 32, None); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/circle_2d.stl", - circle_2d.to_stl_binary("circle_2d").unwrap(), - ); - - let grown_2d = square_2d.offset(0.5); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/square_2d_grow_0_5.stl", - grown_2d.to_stl_ascii("square_2d_grow_0_5"), - ); - - let shrunk_2d = square_2d.offset(-0.5); - let _ = fs::write( - "stl/square_2d_shrink_0_5.stl", - shrunk_2d.to_stl_ascii("square_2d_shrink_0_5"), - ); - - // star(num_points, outer_radius, inner_radius) - let star_2d = CSG::star(5, 2.0, 0.8, None); - let _ = fs::write("stl/star_2d.stl", star_2d.to_stl_ascii("star_2d")); - - // Extrude & Rotate-Extrude - let extruded_star = star_2d.extrude(1.0); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/star_extrude.stl", - extruded_star.to_stl_binary("star_extrude").unwrap(), - ); - - let vector_extruded_star = star_2d.extrude_vector(Vector3::new(2.0, 1.0, 1.0)); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/star_vec_extrude.stl", - vector_extruded_star.to_stl_binary("star_extrude").unwrap(), - ); - - let revolve_circle = circle_2d.translate(10.0, 0.0, 0.0).rotate_extrude(360.0, 32); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/circle_revolve_360.stl", - revolve_circle.to_stl_binary("circle_revolve_360").unwrap(), - ); - - let partial_revolve = circle_2d.translate(10.0, 0.0, 0.0).rotate_extrude(180.0, 32); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/circle_revolve_180.stl", - partial_revolve.to_stl_binary("circle_revolve_180").unwrap(), - ); - - // 9) Subdivide triangles (for smoother sphere or shapes): - let subdiv_sphere = sphere.subdivide_triangles(2.try_into().expect("not 0")); // 2 subdivision levels - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/sphere_subdiv2.stl", - subdiv_sphere.to_stl_binary("sphere_subdiv2").unwrap(), - ); - - // 10) Renormalize polygons (flat shading): - let mut union_clone = union_shape.clone(); - union_clone.renormalize(); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/union_renormalized.stl", - union_clone.to_stl_binary("union_renormalized").unwrap(), - ); - - // 11) Ray intersection demo (just printing the results) - { - let ray_origin = Point3::new(0.0, 0.0, -5.0); - let ray_dir = Vector3::new(0.0, 0.0, 1.0); // pointing along +Z - let hits = cube.ray_intersections(&ray_origin, &ray_dir); - println!("Ray hits on the cube: {:?}", hits); - } - - // 12) Polyhedron example (simple tetrahedron): - let points = [ - [0.0, 0.0, 0.0], - [1.0, 0.0, 0.0], - [0.5, 1.0, 0.0], - [0.5, 0.5, 1.0], - ]; - let faces = vec![ - vec![0, 2, 1], // base triangle - vec![0, 1, 3], // side - vec![1, 2, 3], - vec![2, 0, 3], - ]; - let poly = CSG::polyhedron(&points, &faces, None); - #[cfg(feature = "stl-io")] - let _ = fs::write("stl/tetrahedron.stl", poly.to_stl_ascii("tetrahedron")); - - // 13) Text example (2D). Provide a valid TTF font data below: - // (Replace "asar.ttf" with a real .ttf file in your project.) - #[cfg(feature = "truetype-text")] - let font_data = include_bytes!("../asar.ttf"); - #[cfg(feature = "truetype-text")] - let text_csg = CSG::text("HELLO", font_data, 15.0, None); - #[cfg(feature = "stl-io")] - #[cfg(feature = "truetype-text")] - let _ = fs::write( - "stl/text_hello_2d.stl", - text_csg.to_stl_binary("text_hello_2d").unwrap(), - ); - - // Optionally extrude the text: - #[cfg(feature = "truetype-text")] - let text_extruded = text_csg.extrude(2.0); - #[cfg(feature = "stl-io")] - #[cfg(feature = "truetype-text")] - let _ = fs::write( - "stl/text_hello_extruded.stl", - text_extruded.to_stl_binary("text_hello_extruded").unwrap(), - ); - - // 14) Mass properties (just printing them) - let (mass, com, principal_frame) = cube.mass_properties(1.0); - println!("Cube mass = {}", mass); - println!("Cube center of mass = {:?}", com); - println!("Cube principal inertia local frame = {:?}", principal_frame); - - // 1) Create a cube from (-1,-1,-1) to (+1,+1,+1) - // (By default, CSG::cube(None) is from -1..+1 if the "radius" is [1,1,1].) - let cube = CSG::cube(100.0, 100.0, 100.0, None); - // 2) Flatten into the XY plane - let flattened = cube.flatten(); - let _ = fs::write( - "stl/flattened_cube.stl", - flattened.to_stl_ascii("flattened_cube"), - ); - - // Create a frustum (start=-2, end=+2) with radius1 = 1, radius2 = 2, 32 slices - let frustum = CSG::frustum_ptp( - Point3::new(0.0, 0.0, -2.0), - Point3::new(0.0, 0.0, 2.0), - 1.0, - 2.0, - 32, - None, - ); - let _ = fs::write("stl/frustum.stl", frustum.to_stl_ascii("frustum")); - - // 1) Create a cylinder (start=-1, end=+1) with radius=1, 32 slices - let cyl = CSG::frustum_ptp( - Point3::new(0.0, 0.0, -1.0), - Point3::new(0.0, 0.0, 1.0), - 1.0, - 1.0, - 32, - None, - ); - // 2) Slice at z=0 - #[cfg(feature = "hashmap")] - { - let cross_section = cyl.slice(Plane::from_normal(Vector3::z(), 0.0)); - let _ = fs::write("stl/sliced_cylinder.stl", cyl.to_stl_ascii("sliced_cylinder")); - let _ = fs::write( - "stl/sliced_cylinder_slice.stl", - cross_section.to_stl_ascii("sliced_cylinder_slice"), - ); - } - - //let poor_geometry_shape = moved_cube.difference(&sphere); - //#[cfg(feature = "earclip-io")] - //let retriangulated_shape = poor_geometry_shape.triangulate_earclip(); - //#[cfg(all(feature = "earclip-io", feature = "stl-io"))] - //let _ = fs::write("stl/retriangulated.stl", retriangulated_shape.to_stl_binary("retriangulated").unwrap()); - - let sphere_test = CSG::sphere(1.0, 16, 8, None); - let cube_test = CSG::cube(1.0, 1.0, 1.0, None); - let res = cube_test.difference(&sphere_test); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/sphere_cube_test.stl", - res.to_stl_binary("sphere_cube_test").unwrap(), - ); - assert_eq!(res.bounding_box(), cube_test.bounding_box()); - - #[cfg(all(feature = "stl-io", feature = "metaballs"))] - { - // Suppose we want two overlapping metaballs - let balls = vec![ - MetaBall::new(Point3::origin(), 1.0), - MetaBall::new(Point3::new(1.75, 0.0, 0.0), 1.0), - ]; - - let resolution = (60, 60, 60); - let iso_value = 1.0; - let padding = 1.0; - - #[cfg(feature = "metaballs")] - let metaball_csg = CSG::metaballs(&balls, resolution, iso_value, padding, None); - - // For instance, save to STL - let stl_data = metaball_csg.to_stl_binary("my_metaballs").unwrap(); - std::fs::write("stl/metaballs.stl", stl_data).expect("Failed to write metaballs.stl"); - } - - #[cfg(feature = "sdf")] - { - // Example SDF for a sphere of radius 1.5 centered at (0,0,0) - let my_sdf = |p: &Point3| p.coords.norm() - 1.5; - - let resolution = (60, 60, 60); - let min_pt = Point3::new(-2.0, -2.0, -2.0); - let max_pt = Point3::new(2.0, 2.0, 2.0); - let iso_value = 0.0; // Typically zero for SDF-based surfaces - - let csg_shape = CSG::sdf(my_sdf, resolution, min_pt, max_pt, iso_value, None); - - // Now `csg_shape` is your polygon mesh as a CSG you can union, subtract, or export: - #[cfg(feature = "stl-io")] - let _ = std::fs::write( - "stl/sdf_sphere.stl", - csg_shape.to_stl_binary("sdf_sphere").unwrap(), - ); - } - - // Create a pie slice of radius 2, from 0 to 90 degrees - let wedge = CSG::pie_slice(2.0, 0.0, 90.0, 16, None); - let _ = fs::write("stl/pie_slice.stl", wedge.to_stl_ascii("pie_slice")); - - // Create a 2D "metaball" shape from 3 circles - use nalgebra::Point2; - let balls_2d = vec![ - (Point2::new(0.0, 0.0), 1.0), - (Point2::new(1.5, 0.0), 1.0), - (Point2::new(0.75, 1.0), 0.5), - ]; - let mb2d = CSG::metaballs2d(&balls_2d, (100, 100), 1.0, 0.25, None); - let _ = fs::write("stl/mb2d.stl", mb2d.to_stl_ascii("metaballs2d")); - - // Create a supershape - let sshape = CSG::supershape(1.0, 1.0, 6.0, 1.0, 1.0, 1.0, 128, None); - let _ = fs::write("stl/supershape.stl", sshape.to_stl_ascii("supershape")); - - // Distribute a square along an arc - let square = CSG::circle(1.0, 32, None); - let arc_array = square.distribute_arc(5, 5.0, 0.0, 180.0); - let _ = fs::write("stl/arc_array.stl", arc_array.to_stl_ascii("arc_array")); - - // Distribute that wedge along a linear axis - let wedge_line = wedge.distribute_linear(4, nalgebra::Vector3::new(1.0, 0.0, 0.0), 3.0); - let _ = fs::write("stl/wedge_line.stl", wedge_line.to_stl_ascii("wedge_line")); - - // Make a 4x4 grid of the supershape - let grid_of_ss = sshape.distribute_grid(4, 4, 3.0, 3.0); - let _ = fs::write("stl/grid_of_ss.stl", grid_of_ss.to_stl_ascii("grid_of_ss")); - - // 1. Circle with keyway - let keyway_shape = CSG::circle_with_keyway(10.0, 64, 2.0, 3.0, None); - let _ = fs::write( - "stl/keyway_shape.stl", - keyway_shape.to_stl_ascii("keyway_shape"), - ); - // Extrude it 2 units: - let keyway_3d = keyway_shape.extrude(2.0); - let _ = fs::write("stl/keyway_3d.stl", keyway_3d.to_stl_ascii("keyway_3d")); - - // 2. D-shape - let d_shape = CSG::circle_with_flat(5.0, 32, 2.0, None); - let _ = fs::write("stl/d_shape.stl", d_shape.to_stl_ascii("d_shape")); - let d_3d = d_shape.extrude(1.0); - let _ = fs::write("stl/d_3d.stl", d_3d.to_stl_ascii("d_3d")); - - // 3. Double-flat circle - let double_flat = CSG::circle_with_two_flats(8.0, 64, 3.0, None); - let _ = fs::write("stl/double_flat.stl", double_flat.to_stl_ascii("double_flat")); - let df_3d = double_flat.extrude(0.5); - let _ = fs::write("stl/df_3d.stl", df_3d.to_stl_ascii("df_3d")); - - // A 3D teardrop shape - let teardrop_solid = CSG::teardrop(3.0, 5.0, 32, 32, None); - let _ = fs::write( - "stl/teardrop_solid.stl", - teardrop_solid.to_stl_ascii("teardrop_solid"), - ); - - // A 3D egg shape - let egg_solid = CSG::egg(2.0, 4.0, 8, 16, None); - let _ = fs::write("stl/egg_solid.stl", egg_solid.to_stl_ascii("egg_solid")); - - // An ellipsoid with X radius=2, Y radius=1, Z radius=3 - let ellipsoid = CSG::ellipsoid(2.0, 1.0, 3.0, 16, 8, None); - let _ = fs::write("stl/ellipsoid.stl", ellipsoid.to_stl_ascii("ellipsoid")); - - // A teardrop 'blank' hole - let teardrop_cylinder = CSG::teardrop_cylinder(2.0, 4.0, 32.0, 16, None); - let _ = fs::write( - "stl/teardrop_cylinder.stl", - teardrop_cylinder.to_stl_ascii("teardrop_cylinder"), - ); - - // 1) polygon() - let polygon_2d = CSG::polygon(&[[0.0, 0.0], [2.0, 0.0], [1.5, 1.0], [1.0, 2.0]], None); - let _ = fs::write("stl/polygon_2d.stl", polygon_2d.to_stl_ascii("polygon_2d")); - - // 2) rounded_rectangle(width, height, corner_radius, corner_segments) - let rrect_2d = CSG::rounded_rectangle(4.0, 2.0, 0.3, 8, None); - let _ = fs::write( - "stl/rounded_rectangle_2d.stl", - rrect_2d.to_stl_ascii("rounded_rectangle_2d"), - ); - - // 3) ellipse(width, height, segments) - let ellipse = CSG::ellipse(3.0, 1.5, 32, None); - let _ = fs::write("stl/ellipse.stl", ellipse.to_stl_ascii("ellipse")); - - // 4) regular_ngon(sides, radius) - let ngon_2d = CSG::regular_ngon(6, 1.0, None); // Hexagon - let _ = fs::write("stl/ngon_2d.stl", ngon_2d.to_stl_ascii("ngon_2d")); - - // 6) trapezoid(top_width, bottom_width, height) - let trap_2d = CSG::trapezoid(1.0, 2.0, 2.0, 0.5, None); - let _ = fs::write("stl/trapezoid_2d.stl", trap_2d.to_stl_ascii("trapezoid_2d")); - - // 8) teardrop(width, height, segments) [2D shape] - let teardrop_2d = CSG::teardrop_outline(2.0, 3.0, 16, None); - let _ = fs::write("stl/teardrop_2d.stl", teardrop_2d.to_stl_ascii("teardrop_2d")); - - // 9) egg_outline(width, length, segments) [2D shape] - let egg_2d = CSG::egg_outline(2.0, 4.0, 32, None); - let _ = fs::write( - "stl/egg_outline_2d.stl", - egg_2d.to_stl_ascii("egg_outline_2d"), - ); - - // 10) squircle(width, height, segments) - let squircle_2d = CSG::squircle(3.0, 3.0, 32, None); - let _ = fs::write("stl/squircle_2d.stl", squircle_2d.to_stl_ascii("squircle_2d")); - - // 11) keyhole(circle_radius, handle_width, handle_height, segments) - let keyhole_2d = CSG::keyhole(1.0, 1.0, 2.0, 16, None); - let _ = fs::write("stl/keyhole_2d.stl", keyhole_2d.to_stl_ascii("keyhole_2d")); - - // 12) reuleaux_polygon(sides, side_len, segments) - let reuleaux3_2d = CSG::reuleaux(3, 2.0, 64, None); // Reuleaux triangle - let _ = fs::write( - "stl/reuleaux3_2d.stl", - reuleaux3_2d.to_stl_ascii("reuleaux_2d"), - ); - - // 12) reuleaux_polygon(sides, radius, arc_segments_per_side) - let reuleaux4_2d = CSG::reuleaux(4, 2.0, 64, None); // Reuleaux triangle - let _ = fs::write( - "stl/reuleaux4_2d.stl", - reuleaux4_2d.to_stl_ascii("reuleaux_2d"), - ); - - // 12) reuleaux_polygon(sides, radius, arc_segments_per_side) - let reuleaux5_2d = CSG::reuleaux(5, 2.0, 64, None); // Reuleaux triangle - let _ = fs::write( - "stl/reuleaux5_2d.stl", - reuleaux5_2d.to_stl_ascii("reuleaux_2d"), - ); - - // 13) ring(inner_diam, thickness, segments) - let ring_2d = CSG::ring(5.0, 1.0, 32, None); - let _ = fs::write("stl/ring_2d.stl", ring_2d.to_stl_ascii("ring_2d")); - - // 15) from_image(img, threshold, closepaths, metadata) [requires "image" feature] - #[cfg(feature = "image")] - { - // Make a simple 64x64 gray image with a circle in the center - let mut img: GrayImage = ImageBuffer::new(64, 64); - // Fill a small circle of "white" pixels in the middle - let center = (32, 32); - for y in 0..64 { - for x in 0..64 { - let dx = x as i32 - center.0; - let dy = y as i32 - center.1; - if dx * dx + dy * dy < 15 * 15 { - img.put_pixel(x, y, image::Luma([255u8])); - } - } - } - let csg_img = CSG::from_image(&img, 128, true, None).center(); - let _ = fs::write("stl/from_image.stl", csg_img.to_stl_ascii("from_image")); - } - - // 16) gyroid(...) – uses the current CSG volume as a bounding region - // Let's reuse the `cube` from above: - #[cfg(feature = "stl-io")] - { - let gyroid_inside_cube = hull_of_union - .scale(20.0, 20.0, 20.0) - .gyroid(64, 2.0, 0.0, None); - let _ = fs::write( - "stl/gyroid_cube.stl", - gyroid_inside_cube.to_stl_binary("gyroid_cube").unwrap(), - ); - - let schwarzp_inside_cube = hull_of_union - .scale(20.0, 20.0, 20.0) - .schwarz_p(64, 2.0, 0.0, None); - let _ = fs::write( - "stl/schwarz_p_cube.stl", - schwarzp_inside_cube.to_stl_binary("schwarz_p_cube").unwrap(), - ); - - let schwarzd_inside_cube = hull_of_union - .scale(20.0, 20.0, 20.0) - .schwarz_d(64, 2.0, 0.0, None); - let _ = fs::write( - "stl/schwarz_d_cube.stl", - schwarzd_inside_cube.to_stl_binary("schwarz_d_cube").unwrap(), - ); - } - - // Define the start point and the arrow direction vector. - // The arrow’s length is the norm of the direction vector. - let start = Point3::new(1.0, 1.0, 1.0); - let direction = Vector3::new(10.0, 5.0, 20.0); - - // Define the resolution (number of segments for the cylindrical shaft and head). - let segments = 16; - - // Create the arrow. We pass `None` for metadata. - let arrow_csg = CSG::arrow(start, direction, segments, true, None::<()>); - let _ = fs::write("stl/arrow.stl", arrow_csg.to_stl_ascii("arrow_example")); - - let arrow_reversed_csg = CSG::arrow(start, direction, segments, false, None::<()>); - let _ = fs::write( - "stl/arrow_reversed.stl", - arrow_reversed_csg.to_stl_ascii("arrow_example"), - ); - - // 2-D profile for NACA 2412, 1 m chord, 100 pts / surface - let naca2412 = CSG::airfoil("2412", 1.0, 100, None); - let _ = fs::write("stl/naca2412.stl", naca2412.to_stl_ascii("2412")); - - // quick solid wing rib 5 mm thick - let rib = naca2412.extrude(0.005); - let _ = fs::write("stl/naca2412_extruded.stl", rib.to_stl_ascii("2412_extruded")); - - // symmetric foil for a centerboard - let naca0015 = CSG::airfoil("0015", 0.3, 80, None) - .extrude_vector(nalgebra::Vector3::new(0.0, 0.0, 1.2)); - let _ = fs::write("stl/naca0015.stl", naca0015.to_stl_ascii("naca0015")); - - let oct = CSG::octahedron(10.0, None); - let _ = fs::write("stl/octahedron.stl", oct.to_stl_ascii("octahedron")); - - //let dodec = CSG::dodecahedron(15.0, None); - //let _ = fs::write("stl/dodecahedron.stl", dodec.to_stl_ascii("")); - - let ico = CSG::icosahedron(12.0, None); - let _ = fs::write("stl/icosahedron.stl", ico.to_stl_ascii("")); - - let torus = CSG::torus(20.0, 5.0, 48, 24, None); - let _ = fs::write("stl/torus.stl", torus.to_stl_ascii("")); - - let heart2d = CSG::heart(30.0, 25.0, 128, None); - let _ = fs::write("stl/heart2d.stl", heart2d.to_stl_ascii("")); - - let crescent2d = CSG::crescent(10.0, 7.0, 4.0, 64, None); - let _ = fs::write("stl/crescent2d.stl", crescent2d.to_stl_ascii("")); - - // --------------------------------------------------------- - // Additional “SCENES” Demonstrating Each Function Minimally - // - // In these scenes, we typically: - // 1) Create the shape - // 2) Extrude (if 2D) so we can save an STL - // 3) Optionally union with a small arrow that points to - // a location of interest in the shape - // 4) Save the result as an STL, e.g. "scene_XX_something.stl" - // - // Because many shapes are already shown above, these are - // just short examples to help with explanation. - // --------------------------------------------------------- - - // Scene A: Demonstrate a right_triangle(width=2, height=1) - { - let tri_2d = CSG::right_triangle(2.0, 1.0, None); - // A tiny arrow pointing from the right-angle corner outward: - let arrow = CSG::arrow( - Point3::new(0.0, 0.0, 0.1), // at corner - Vector3::new(0.5, 0.0, 0.0), - 8, - true, - None::<()>, - ) - .scale(0.05, 0.05, 0.05); - let scene = tri_2d.extrude(0.1).union(&arrow); - let _ = fs::write( - "stl/scene_right_triangle.stl", - scene.to_stl_ascii("scene_right_triangle"), - ); - } - - // Scene B: Demonstrate extrude_vector(direction) - { - let circle2d = CSG::circle(1.0, 32, None); - // extrude along an arbitrary vector - let extruded_along_vec = circle2d.extrude_vector(Vector3::new(0.0, 0.0, 2.0)); - let _ = fs::write( - "stl/scene_extrude_vector.stl", - extruded_along_vec.to_stl_ascii("scene_extrude_vector"), - ); - } - - // Scene E: Demonstrate center() (moves shape so bounding box is centered on the origin) - { - let off_center_circle = CSG::circle(1.0, 32, None) - .translate(5.0, 2.0, 0.0) - .extrude(0.1); - let centered_circle = off_center_circle.center(); - let _ = fs::write( - "stl/scene_circle_off_center.stl", - off_center_circle.to_stl_ascii("scene_circle_off_center"), - ); - let _ = fs::write( - "stl/scene_circle_centered.stl", - centered_circle.to_stl_ascii("scene_circle_centered"), - ); - } - - // Scene F: Demonstrate float() (moves shape so bottom is at z=0) - { - let sphere_for_float = CSG::sphere(1.0, 16, 8, None).translate(0.0, 0.0, -1.5); - let floated = sphere_for_float.float(); - let _ = fs::write( - "stl/scene_sphere_before_float.stl", - sphere_for_float.to_stl_ascii("scene_sphere_before_float"), - ); - let _ = fs::write( - "stl/scene_sphere_floated.stl", - floated.to_stl_ascii("scene_sphere_floated"), - ); - } - - // Scene G: Demonstrate inverse() (flips inside/outside) - { - // Hard to visualize in STL, but let's do it anyway - let inv_sphere = sphere.inverse(); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/scene_inverse_sphere.stl", - inv_sphere.to_stl_binary("scene_inverse_sphere").unwrap(), - ); - } - - // Scene H: Demonstrate tessellate() (forces triangulation) - { - let tri_sphere = sphere.tessellate(); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/scene_tessellate_sphere.stl", - tri_sphere.to_stl_binary("scene_tessellate_sphere").unwrap(), - ); - } - - // Scene I: Demonstrate slice(plane) – slice a cube at z=0 - { - let plane_z = Plane::from_normal(Vector3::z(), 0.5); - let sliced_polygons = cube.slice(plane_z); - let _ = fs::write("stl/scene_sliced_cube.stl", cube.to_stl_ascii("sliced_cube")); - // Save cross-section as well - let _ = fs::write( - "stl/scene_sliced_cube_section.stl", - sliced_polygons.to_stl_ascii("sliced_cube_section"), - ); - } - - // Scene J: Demonstrate re-computing vertices() or printing them - { - let circle_extruded = CSG::circle(1.0, 32, None).extrude(0.5); - let verts = circle_extruded.vertices(); - println!("Scene J circle_extruded has {} vertices", verts.len()); - // We'll still save an STL so there's a visual - let _ = fs::write( - "stl/scene_j_circle_extruded.stl", - circle_extruded.to_stl_ascii("scene_j_circle_extruded"), - ); - } - - // Scene K: Demonstrate reuleaux_polygon with a typical triangle shape - // (already used sides=4 above, so let's do sides=3 here) - { - let reuleaux_tri = CSG::reuleaux(3, 2.0, 16, None).extrude(0.1); - let _ = fs::write( - "stl/scene_reuleaux_triangle.stl", - reuleaux_tri.to_stl_ascii("scene_reuleaux_triangle"), - ); - } - - // Scene L: Demonstrate rotate_extrude (360 deg) on a square - { - let small_square = CSG::square(1.0, 1.0, None).translate(2.0, 0.0, 0.0); - let revolve = small_square.rotate_extrude(360.0, 24); - let _ = fs::write( - "stl/scene_square_revolve_360.stl", - revolve.to_stl_ascii("scene_square_revolve_360"), - ); - } - - // Scene M: Demonstrate “mirror” across a Y=0 plane - { - let plane_y = Plane::from_normal(Vector3::y(), 0.0); - let shape = CSG::square(2.0, 1.0, None) - .translate(1.0, 1.0, 0.0) - .extrude(0.1); - let mirrored = shape.mirror(plane_y); - let _ = fs::write( - "stl/scene_square_mirrored_y.stl", - mirrored.to_stl_ascii("scene_square_mirrored_y"), - ); - } - - // Scene N: Demonstrate scale() - { - let scaled = sphere.scale(1.0, 2.0, 0.5); - #[cfg(feature = "stl-io")] - let _ = fs::write( - "stl/scene_scaled_sphere.stl", - scaled.to_stl_binary("scene_scaled_sphere").unwrap(), - ); - } - - // Scene O: Demonstrate transform() with an arbitrary affine matrix - { - use nalgebra::{Matrix4, Translation3}; - let xlate = Translation3::new(2.0, 0.0, 1.0).to_homogeneous(); - // Scale matrix - let scale_mat = Matrix4::new_scaling(0.5); - // Combine - let transform_mat = xlate * scale_mat; - let shape = CSG::cube(1.0, 1.0, 1.0, None).transform(&transform_mat); - let _ = fs::write( - "stl/scene_transform_cube.stl", - shape.to_stl_ascii("scene_transform_cube"), - ); - } - - // Scene P: Demonstrate offset(distance) - { - let poly_2d = CSG::polygon(&[[0.0, 0.0], [2.0, 0.0], [1.0, 1.5]], None); - let grown = poly_2d.offset(0.2); - let scene = grown.extrude(0.1); - let _ = fs::write( - "stl/scene_offset_grown.stl", - scene.to_stl_ascii("scene_offset_grown"), - ); - } - - let gear_involute_2d = CSG::involute_gear_2d( - 2.0, // module [mm] - 20, // z – number of teeth - 20.0, // α – pressure angle [deg] - 0.05, // radial clearance - 0.02, // backlash at pitch line - 14, // segments per involute flank - None, - ); - let _ = fs::write( - "stl/gear_involute_2d.stl", - gear_involute_2d.to_stl_ascii("gear_involute_2d"), - ); - - let gear_cycloid_2d = CSG::cycloidal_gear_2d( - 2.0, // module - 17, // gear teeth - 18, // mating pin-wheel teeth (zₚ = z±1) - 0.05, // clearance - 20, // segments per flank - None, - ); - let _ = fs::write( - "stl/gear_cycloid_2d.stl", - gear_cycloid_2d.to_stl_ascii("gear_cycloid_2d"), - ); - - let rack_involute = CSG::involute_rack_2d( - 2.0, // module - 12, // number of rack teeth to generate - 20.0, // pressure angle - 0.05, // clearance - 0.02, // backlash - None, - ); - let _ = fs::write( - "stl/rack_involute.stl", - rack_involute.to_stl_ascii("rack_involute"), - ); - - let rack_cycloid = CSG::cycloidal_rack_2d( - 2.0, // module - 12, // teeth - 1.0, // generating-circle radius (≈ m/2 for a conventional pin-rack) - 0.05, // clearance - 24, // segments per flank - None, - ); - let _ = fs::write( - "stl/rack_cycloid.stl", - rack_cycloid.to_stl_ascii("rack_cycloid"), - ); - - let spur_involute = CSG::spur_gear_involute( - 2.0, 20, 20.0, 0.05, 0.02, 14, 12.0, // face-width (extrusion thickness) - None, - ); - let _ = fs::write( - "stl/spur_involute.stl", - spur_involute.to_stl_ascii("spur_involute"), - ); - - let spur_cycloid = CSG::spur_gear_cycloid( - 2.0, 17, 18, 0.05, 20, 12.0, // thickness - None, - ); - let _ = fs::write( - "stl/spur_cycloid.stl", - spur_cycloid.to_stl_ascii("spur_cycloid"), - ); - - /* - let helical = CSG::helical_involute_gear( - 2.0, // module - 20, // z - 20.0, // pressure angle - 0.05, 0.02, 14, - 25.0, // face-width - 15.0, // helix angle β [deg] - 40, // axial slices (resolution of the twist) - None, - ); - let _ = fs::write("stl/helical.stl", helical.to_stl_ascii("helical")); - */ - - // --------------------------------------------------------------------- - // Bézier curve demo ---------------------------------------------------- - let bezier_ctrl = &[ - [0.0, 0.0], // P0 - [1.0, 2.0], // P1 - [3.0, 3.0], // P2 - [4.0, 0.0], // P3 - ]; - let bezier_2d = CSG::bezier(bezier_ctrl, 128, None); - let _ = fs::write("stl/bezier_2d.stl", bezier_2d.to_stl_ascii("bezier_2d")); - - // give it a little “body” so we can see it in a solid viewer - let bezier_3d = bezier_2d.extrude(0.25); - let _ = fs::write( - "stl/bezier_extruded.stl", - bezier_3d.to_stl_ascii("bezier_extruded"), - ); - - // --------------------------------------------------------------------- - // B-spline demo -------------------------------------------------------- - let bspline_ctrl = &[[0.0, 0.0], [1.0, 2.5], [3.0, 3.0], [5.0, 0.0], [6.0, -1.5]]; - let bspline_2d = CSG::bspline( - bspline_ctrl, - /* degree p = */ 3, - /* seg/span */ 32, - None, - ); - let _ = fs::write("stl/bspline_2d.stl", bspline_2d.to_stl_ascii("bspline_2d")); - - #[cfg(feature = "bevymesh")] - println!("{:#?}", bezier_3d.to_bevy_mesh()); - - // a quick thickening just like the Bézier - //let bspline_3d = bspline_2d.extrude(0.25); - //let _ = fs::write( - // "stl/bspline_extruded.stl", - // bspline_3d.to_stl_ascii("bspline_extruded"), - //); - - // Done! - println!( - "All scenes have been created and written to the 'stl' folder (where applicable)." - ); -} diff --git a/src/plane.rs b/src/plane.rs index b1736d1..2d38d26 100644 --- a/src/plane.rs +++ b/src/plane.rs @@ -34,6 +34,74 @@ pub struct Plane { pub point_c: Point3, } +impl PartialEq for Plane { + fn eq(&self, other: &Self) -> bool { + if self.point_a == other.point_a + && self.point_b == other.point_b + && self.point_c == other.point_c + { + true + } else { + // check if co-planar + robust::orient3d( + point_to_coord3d(self.point_a), + point_to_coord3d(self.point_b), + point_to_coord3d(self.point_c), + point_to_coord3d(other.point_a), + ) == 0.0 + && robust::orient3d( + point_to_coord3d(self.point_a), + point_to_coord3d(self.point_b), + point_to_coord3d(self.point_c), + point_to_coord3d(other.point_b), + ) == 0.0 + && robust::orient3d( + point_to_coord3d(self.point_a), + point_to_coord3d(self.point_b), + point_to_coord3d(self.point_c), + point_to_coord3d(other.point_c), + ) == 0.0 + } + } + + fn ne(&self, other: &Self) -> bool { + if self.point_a != other.point_a + || self.point_b != other.point_b + || self.point_c != other.point_c + { + true + } else { + // check if co-planar + robust::orient3d( + point_to_coord3d(self.point_a), + point_to_coord3d(self.point_b), + point_to_coord3d(self.point_c), + point_to_coord3d(other.point_a), + ) != 0.0 + || robust::orient3d( + point_to_coord3d(self.point_a), + point_to_coord3d(self.point_b), + point_to_coord3d(self.point_c), + point_to_coord3d(other.point_b), + ) != 0.0 + || robust::orient3d( + point_to_coord3d(self.point_a), + point_to_coord3d(self.point_b), + point_to_coord3d(self.point_c), + point_to_coord3d(other.point_c), + ) != 0.0 + } + } +} + +fn point_to_coord3d(point: Point3) -> robust::Coord3D { + robust::Coord3D { + x: point.coords.x, + y: point.coords.y, + z: point.coords.z, + } +} + impl Plane { /// Tries to pick three vertices that span the largest area triangle /// (maximally well-spaced) and returns a plane defined by them. @@ -219,7 +287,7 @@ impl Plane { // ──────────────────────────────────────────────────────────────── // Robust polygon split // ──────────────────────────────────────────────────────────────── - /// + /// Returns four buckets: /// `(coplanar_front, coplanar_back, front, back)`. pub fn split_polygon( diff --git a/src/polygon.rs b/src/polygon.rs index 389266c..ac78c13 100644 --- a/src/polygon.rs +++ b/src/polygon.rs @@ -23,6 +23,14 @@ pub struct Polygon { pub metadata: Option, } +impl PartialEq for Polygon { + fn eq(&self, other: &Self) -> bool { + self.vertices == other.vertices + && self.plane == other.plane + && self.metadata == other.metadata + } +} + impl Polygon { /// Create a polygon from vertices pub fn new(vertices: Vec, metadata: Option) -> Self { diff --git a/src/tests.rs b/src/tests.rs index 46c4788..2d703eb 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -101,6 +101,11 @@ fn test_to_stl_ascii() { assert!(stl_str.contains("facet normal")); // Should contain some vertex lines assert!(stl_str.contains("vertex")); + + assert_eq!( + CSG::from_stl(stl_str.as_bytes(), None).unwrap().polygons, + cube.tessellate().polygons + ); } // -------------------------------------------------------- @@ -978,27 +983,17 @@ fn test_csg_to_rigid_body() { } #[test] -fn test_csg_to_stl_and_from_stl_file() -> Result<(), Box> { - // We'll create a small shape, write to an STL, read it back. - // You can redirect to a temp file or do an in-memory test. - let tmp_path = "test_csg_output.stl"; - +fn test_csg_to_stl_and_from_stl_file() { let cube: CSG<()> = CSG::cube(2.0, 2.0, 2.0, None); - let res = cube.to_stl_binary("A cube"); - let _ = std::fs::write(tmp_path, res.as_ref().unwrap()); - assert!(res.is_ok()); - - let stl_data: Vec = std::fs::read(tmp_path)?; - let csg_in: CSG<()> = CSG::from_stl(&stl_data, None)?; - // We expect to read the same number of triangular faces as the cube originally had - // (though the orientation/normals might differ). + let res = cube.to_stl_binary("A cube").unwrap(); + + let csg_from_stl: CSG<()> = CSG::from_stl(&res, None).unwrap(); + // The default cube -> 6 polygons x 1 polygon each with 4 vertices => 12 triangles in STL. // So from_stl_file => we get 12 triangles as 12 polygons (each is a tri). - assert_eq!(csg_in.polygons.len(), 12); + assert_eq!(csg_from_stl.polygons.len(), 12); - // Cleanup the temp file if desired - let _ = std::fs::remove_file(tmp_path); - Ok(()) + assert_eq!(csg_from_stl.polygons, cube.tessellate().polygons); } /// A small, custom metadata type to demonstrate usage. @@ -1279,7 +1274,7 @@ fn test_complex_metadata_struct_in_boolean_ops() { /// Helper function to calculate the signed area of a polygon. /// Positive area indicates CCW ordering. -fn signed_area(polygon: &Polygon<()>) -> Real { +fn signed_area_2d(polygon: &Polygon<()>) -> Real { let mut area = 0.0; let verts = &polygon.vertices; for i in 0..verts.len() { @@ -1292,9 +1287,11 @@ fn signed_area(polygon: &Polygon<()>) -> Real { #[test] fn test_square_ccw_ordering() { let square = CSG::square(2.0, 2.0, None); + assert_eq!(square.polygons.len(), 1); let poly = &square.polygons[0]; - let area = signed_area(poly); + // todo needs to be a 2d test + polygon 3d area would be nice + let area = signed_area_2d(poly); assert!(area > 0.0, "Square vertices are not CCW ordered"); } @@ -1307,7 +1304,7 @@ fn test_offset_2d_positive_distance_grows() { // The offset square should have area greater than 4.0 assert_eq!(offset.polygons.len(), 1); let poly = &offset.polygons[0]; - let area = signed_area(poly); + let area = signed_area_2d(poly); assert!( area > 4.0, "Offset with positive distance did not grow the square" @@ -1323,7 +1320,7 @@ fn test_offset_2d_negative_distance_shrinks() { // The offset square should have area less than 4.0 assert_eq!(offset.polygons.len(), 1); let poly = &offset.polygons[0]; - let area = signed_area(poly); + let area = signed_area_2d(poly); assert!( area < 4.0, "Offset with negative distance did not shrink the square" @@ -1338,7 +1335,7 @@ fn test_polygon_2d_enforce_ccw_ordering() { // Enforce CCW ordering csg_cw.renormalize(); let poly = &csg_cw.polygons[0]; - let area = signed_area(poly); + let area = signed_area_2d(poly); assert!(area > 0.0, "Polygon ordering was not corrected to CCW"); } @@ -1350,8 +1347,8 @@ fn test_circle_offset_2d() { // Original circle has area ~3.1416 let original_area = 3.141592653589793; - let grow_area = signed_area(&offset_grow.polygons[0]); - let shrink_area = signed_area(&offset_shrink.polygons[0]); + let grow_area = signed_area_2d(&offset_grow.polygons[0]); + let shrink_area = signed_area_2d(&offset_shrink.polygons[0]); assert!( grow_area > original_area, diff --git a/src/vertex.rs b/src/vertex.rs index a14d69c..a4e587d 100644 --- a/src/vertex.rs +++ b/src/vertex.rs @@ -2,7 +2,7 @@ use crate::float_types::Real; use nalgebra::{Point3, Vector3}; /// A vertex of a polygon, holding position and normal. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Vertex { pub pos: Point3, pub normal: Vector3,