a-givertzman / mdmt-server

Marine Design & Math Tools
GNU General Public License v2.0
0 stars 0 forks source link

Back | 3dModel | opencascade-rs #6

Closed novartole closed 1 month ago

novartole commented 2 months ago
novartole commented 2 months ago

What exactly cxx_name = do?

_rustname and _cxxname are attributes for renaming functions, opaque types, shared structs and enums, and enum variants. For instance, consider a function defined in extern "c++ {}, which should be called from Rust code. It's definition might be annotated with either _#[cxx_name = "funccpp"] or _#[rustname = "..."] attribute. The first one allows to redirect call to another function named _funccpp, which must be defined at C++ side. The second one allows to use another name when calling at Rust side. More details here.

novartole commented 2 months ago

When ~ called (if applied): automatically, manually?

Calling a function or method of C++ world (including object creation) can only be done via smart pointers of cxx (SharedPtr, UniquePtr, etc.). The smart pointers take care about freeing memory properly. For instance, if a variable goes out of scope in Rust, its appropriate destructor is called at C++ side.

novartole commented 2 months ago

Life cycle of C++ objects.

Custom structures (smart pointers) of cxx are greatly described in documentation.

novartole commented 2 months ago

Code quality.

Code source is well organized especially in build related parts. It also seems as a good choice for small projects (as the author originally created the crate for). Intense development may lead to loads of logic duplicates of OCCT and its Rust representation.

opencascade-rs structure overview:

novartole commented 1 month ago

How to optimize operations (Boolean, Splitter, etc.)?

Implementation of boolean operations:

Useful links:

novartole commented 1 month ago

SetUseOBB.

From docs(see Bounding box section): Bounding boxes are used in many OCCT algorithms. The most common use is as a filter avoiding check of excess interferences between pairs of shapes (check of interferences between bounding boxes is much simpler then between shapes and if they do not interfere then there is no point in searching interferences between the corresponding shapes). Generally, bounding boxes can be divided into two main types:

Tests (average time):

novartole commented 1 month ago

SetFuzzyValue.

Generally, it shouldn't be used as an optimization option. The real goal is to fix modeling mistakes.

From docs (see Fuzzy Boolean Operation section): Fuzzy Boolean operation is the option of Basic Operations such as General Fuse, Splitting, Boolean, Section, Maker Volume and Cells building operations, in which additional user-specified tolerance is used. This option allows operators to handle robustly cases of touching and near-coincident, misaligned entities of the arguments.

The Fuzzy option is useful on the shapes with gaps or embeddings between the entities of these shapes, which are not covered by the tolerance values of these entities. Such shapes can be the result of modeling mistakes, or translating process, or import from other systems with loss of precision, or errors in some algorithms.

novartole commented 1 month ago

SetRunParallel.

This option is the real game changer. It affects both on functional and STEP modeling algorithms. Moreover, in INTEL machines additional set true of USE_TBB before compiling OCCT gives even more speed gain:

Useful links:

novartole commented 1 month ago

SetGlueMode.

Setting this mode to non-default may increase speed of Boolean operations in case of shifted or overlapping objects, but it shouldn't be used with Intersection. More details here.

bschwind commented 1 month ago

Hi!

I'm the author of the opencascade-rs bindings, please let me know if you have any questions :)

It's generally usable, but I would say it's still in the experimentation phase of figuring out the best way to organize bindings, especially if I ever want to automate the generation of those bindings. Right now they're all thrown into one cxx.rs bridge file, and one wrapper.hxx file. I could probably split these up into sensible modules, at the very least.

The major thing I've been focusing on is building out a WASM API to allow for more rapid iteration when modeling with Rust code directly. The vanilla Rust API should more or less get developed in lockstep with that API though so no worries there.

All of this has so far been developed with code-based CAD as the main goal, similar to OpenSCAD. But of course that doesn't exclude other crates using it for their own geometrical processing purposes!

Nice research, by the way! It was interesting to read all these evaluation notes on my own project from someone I don't know.

novartole commented 1 month ago

Hi @bschwind ,

Nice to see you here! Thanks for your reply and great tool! It builds like a charm and its structure looks pretty intuitive, so there have been no questions so far, even after almost a month of playing with it.

Modularity is a topic I also think about. Such reusing existing binding types could help in a way to separate, say, mandatory logic and some user API. I have a plan to try my hand at deeper research in this area. I would share results if you're interested.

The purpose for which we use the crate, is to import a STEP model, apply 3D transformations if necessary and then measure some related metrics such as length, area, etc. Unfortunately, several purely rust-written 3D engines that I've tried are either at too early stage (and don't even have stable support of boolean operations), or give artifacts when importing and further work doesn't make any sense. Fortunately, your solution fits many requirements nicely even it's still in the experimentation phase!

novartole commented 1 month ago

Way to integrate measurement module (length, area, etc.).

GProp_GProps class us used for this purpose. Its result depends on dimension and type of object it calls on.

Here is an example taken from opencascade-rs, where Mass() result turns into area.

novartole commented 1 month ago

Way to extend the crate.

Results of last two section can be wrapped up into the following example. Please pay attention on Required changes section below.

Crate: try-occt-rs

src/main.rs

mod cli;

use std::time::Instant;

use clap::Parser;
use cli::{Cli, Cmd, Examples, Orientation};
use glam::dvec3;
use opencascade::{
    primitives::{Compound, Edge, IntoShape, Shape, Solid},
    workplane::Workplane,
};

fn main() {
    let args = Cli::parse();

    // grab result into one model object
    let output_model = {
        // target is usually an input model,
        // result is result model of chosen cmd
        let (target, result) = match args.command {
            Cmd::Volume {
                orientation,
                width,
                heigh,
                depth,
                input,
            } => {
                // expect model to be Shell internally
                let model = Shape::read_step(&input).expect("Failed while reading STEP");
                // plane should intersect target model
                let plane = build_plane(orientation, width, heigh, depth);
                // if two models aren't intersected, it gives nothing as result
                let under_plane = Solid::volume([&model, &plane], true).into_shape();

                // cacl some metrics:
                // - total area of result of volume operation (VO)
                // - area of plane, which is an argument of VO
                // - area of result model of VO - area of argument plane
                let (total, top_plane, valume) = calc_volumed_area(&under_plane, &plane);
                println!(
                    "AREA (sq. m.):\n- total={:.4}\n- top plane={:.4}\n- under plane solid={:.4}",
                    total, top_plane, valume
                );

                (model, under_plane)
            }
            Cmd::Intersect {
                orientation,
                heigh,
                width,
                depth,
                input,
            } => {
                let model = Shape::read_step(input).expect("Failed while reading STEP");
                let plane = build_plane(orientation, width, heigh, depth);

                #[cfg(feature = "verbose")]
                let instant = Instant::now();

                // use extended implementation of intersection
                // to take into account params of operation builder
                let result = model
                    .intersect_with_params(
                        &plane,
                        args.run_parallel,
                        args.fuzzy_value,
                        args.use_obb,
                        args.glue,
                    )
                    .into_shape();

                #[cfg(feature = "verbose")]
                dbg!(instant.elapsed());

                (model, result)
            }
            #[cfg(feature = "verbose")]
            Cmd::Example { name } => match name {
                Examples::CableBracketSelfIntersection => {
                    let model_a = examples::cable_bracket::shape();
                    let model_b = examples::cable_bracket::shape();

                    let instant = Instant::now();
                    let result = model_b.intersect_with_params(
                        &model_a,
                        args.run_parallel,
                        args.fuzzy_value,
                        args.use_obb,
                        args.glue,
                    );
                    dbg!(
                        instant.elapsed(),
                        args.run_parallel,
                        args.fuzzy_value,
                        args.use_obb,
                        args.glue
                    );

                    (model_b, result.into_shape())
                }
            },
        };

        // bake target and result
        if args.join_all {
            Compound::from_shapes([target, result]).into_shape()
        // remain only result edges
        } else if args.only_edges {
            Compound::from_shapes(result.edges().map(Edge::into_shape)).into_shape()
        // don't format result
        } else {
            result
        }
    };

    output_model
        .write_step(args.output)
        .expect("Failed while writting STEP");
}

/// Build a plane to use with boolean and volume operations.
fn build_plane(o: Orientation, w: f64, h: f64, d: f64) -> Shape {
    let (plane, dir) = match o {
        Orientation::Xy => (Workplane::xy(), dvec3(0.0, 0.0, d)),
        Orientation::Xz => (Workplane::xz(), dvec3(0.0, d, 0.0)),
        Orientation::Yz => (Workplane::yz(), dvec3(0.0, 0.0, d)),
    };

    plane.translated(dir).rect(w, h).to_face().into_shape()
}

/// Calculate areas of volume operation result.
/// It might take time due to call of boolean operation internally.
fn calc_volumed_area(shape: &Shape, top_plane: &Shape) -> (f64, f64, f64) {
    // normalize to square meters
    let normalizer = 1e6;

    let top_plane_area = shape
        .intersect(top_plane)
        .faces()
        .next()
        .unwrap()
        .surface_area();
    let total_area: f64 = shape.faces().map(|face| face.surface_area()).sum();

    (
        // total area of shape
        total_area / normalizer,
        // area of shape and plane intersection
        top_plane_area / normalizer,
        // total are - plane area
        (total_area - top_plane_area) / normalizer,
    )
}

src/cli.rs

use clap::{Parser, Subcommand, ValueEnum};

use std::path::PathBuf;

#[derive(ValueEnum, Clone)]
pub enum Orientation {
    Xy,
    Xz,
    Yz,
}

#[derive(ValueEnum, Clone)]
pub enum Examples {
    /// Common of cable_bracket and keycap models
    #[clap(name = "CabBrSelfInt")]
    CableBracketSelfIntersection,
}
/// Examples:
/// - build solid by intersecting plane and hull shell model:
///   ```bash
///   cargo run -rF verbose -- volume xy 250000.0 250000.0 5000.0 ../3ds/hull_shell_centered.step
///   ```
/// - get result face joined with original model after intersection of plane and hull shell:
///   ```bash
///   cargo run -rF verbose -- \
///     --run-parallel --join-all \
///     intersect yz 200000.0 200000.0 112000.0 ../3ds/hull_shell_centered.step
///   ```
/// - run prepared example:
///   ```bash
///   cargo run -rF verbose -- --glue=2 --fuzzy-value=1e-2 example CabBrSelfInt
///   ```
#[derive(Parser, Clone)]
pub struct Cli {
    /// Output file
    #[clap(short, long, default_value = "output.step")]
    pub output: PathBuf,

    /// Join all parts
    #[clap(long, default_value_t = false, group = "view")]
    pub join_all: bool,

    /// Save only edges
    #[clap(long, default_value_t = false, group = "view")]
    pub only_edges: bool,

    #[command(subcommand)]
    pub command: Cmd,

    /// Parallel optimization
    #[clap(long)]
    pub run_parallel: bool,

    /// OBB usage
    #[clap(long)]
    pub use_obb: bool,

    /// Glue option
    #[clap(long, default_value_t = 0)]
    pub glue: u8,

    /// Fuzzy value (aka tolerance) for boolean operations
    #[clap(long, default_value_t = 1e-7)]
    pub fuzzy_value: f64,
}

#[derive(Subcommand, Clone)]
pub enum Cmd {
    /// Split shell using Volume maker
    Volume {
        orientation: Orientation,
        width: f64,
        heigh: f64,
        depth: f64,

        /// Input file path
        #[clap(value_parser = input_parser)]
        input: PathBuf,
    },

    /// Build plane intersection
    Intersect {
        orientation: Orientation,
        width: f64,
        heigh: f64,
        depth: f64,

        /// Input file path
        #[clap(value_parser = input_parser)]
        input: PathBuf,
    },

    /// Prepared examples
    #[cfg(feature = "verbose")]
    Example { name: Examples },
}

fn input_parser(arg: &str) -> anyhow::Result<PathBuf> {
    use anyhow::bail;
    use std::{os::unix::ffi::OsStrExt, str::FromStr};

    let input = PathBuf::from_str(arg)?;

    if !matches!(
        input.extension().map(OsStrExt::as_bytes),
        Some(b"stp" | b"step")
    ) {
        bail!("Supported output format is .step (.stp).");
    }

    Ok(input)
}

Cargo.toml

[package]
name = "try-occt-rs"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1.0.86"
cxx = "1.0.128"

[dependencies.glam]
version = "0.24.2"
features = ["bytemuck"]

[dependencies.clap]
version = "4"
features = ["derive"]

[dependencies.opencascade]
version = "0.2"
path = "../opencascade-rs/crates/opencascade"

[dependencies.examples]
version = "0.2.0"
path = "../opencascade-rs/examples"

[features]
default = ["verbose"]
verbose = []

Required changes in opencascade-rs crate

I would suggest to clone repository to the root folder of try-occt-rs crate (see above) and then apply the changes. Tested with this commit.

crates/occt-sys/build.rs (before/after)

        .define("USE_TBB", "FALSE")
        .define("USE_TBB", "TRUE")

crates/opencascade-sys/include/wrapper.hxx

inline void shape_list_append_shape(TopTools_ListOfShape &list, const TopoDS_Shape &shape) { list.Append(shape); }

inline void SetRunParallel_BRepAlgoAPI_Common(BRepAlgoAPI_Common &theBOP, bool theFlag) {
  theBOP.SetRunParallel(theFlag);
}
inline void SetUseOBB_BRepAlgoAPI_Common(BRepAlgoAPI_Common &theBOP, bool theFlag) { theBOP.SetUseOBB(theFlag); }
inline void SetFuzzyValue_BRepAlgoAPI_Common(BRepAlgoAPI_Common &theBOP, double theFuzz) {
  theBOP.SetFuzzyValue(theFuzz);
}

inline bool HasErrors_BRepAlgoAPI_Common(const BRepAlgoAPI_Common &theBOP) { return theBOP.HasErrors(); }

inline const TopoDS_Shape &BOPAlgo_MakerVolume_Shape(const BOPAlgo_MakerVolume &aMV) { return aMV.Shape(); }

crates/opencascade-sys/src/lib.rs

pub mod ffi {
    #[derive(Debug)]
    #[repr(u32)]
    pub enum BOPAlgo_Operation {
        BOPAlgo_COMMON,
        BOPAlgo_FUSE,
        BOPAlgo_CUT,
        BOPAlgo_CUT21,
        BOPAlgo_SECTION,
        BOPAlgo_UNKNOWN,
    }
/* ... */
        pub fn shape_list_append_shape(list: Pin<&mut TopTools_ListOfShape>, face: &TopoDS_Shape);
/* ... */
        type BOPAlgo_MakerVolume;

        #[cxx_name = "construct_unique"]
        pub fn BOPAlgo_MakerVolume_ctor() -> UniquePtr<BOPAlgo_MakerVolume>;
        pub fn SetArguments(self: Pin<&mut BOPAlgo_MakerVolume>, the_ls: &TopTools_ListOfShape);
        pub fn Perform(self: Pin<&mut BOPAlgo_MakerVolume>, the_range: &Message_ProgressRange);
        pub fn BOPAlgo_MakerVolume_Shape(theMV: &BOPAlgo_MakerVolume) -> &TopoDS_Shape;
/* ... */
        type BOPAlgo_Operation;

        #[cxx_name = "construct_unique"]
        pub fn BRepAlgoAPI_Common_ctor() -> UniquePtr<BRepAlgoAPI_Common>;
/* ... */
        /// Obsolete.
        #[rust_name = "BRepAlgoAPI_Common_ctor2"]
        pub fn Build(self: Pin<&mut BRepAlgoAPI_Common>, the_range: &Message_ProgressRange);
        pub fn SetTools(self: Pin<&mut BRepAlgoAPI_Common>, the_ls: &TopTools_ListOfShape);
        pub fn SetArguments(self: Pin<&mut BRepAlgoAPI_Common>, the_ls: &TopTools_ListOfShape);
        pub fn HasErrors_BRepAlgoAPI_Common(the_bop: &BRepAlgoAPI_Common) -> bool;
        pub fn SetFuzzyValue_BRepAlgoAPI_Common(
            the_bop: Pin<&mut BRepAlgoAPI_Common>,
            the_fuzz: f64,
        );
        pub fn SetRunParallel_BRepAlgoAPI_Common(
            the_bop: Pin<&mut BRepAlgoAPI_Common>,
            the_flag: bool,
        );
        pub fn SetUseOBB_BRepAlgoAPI_Common(
            the_bop: Pin<&mut BRepAlgoAPI_Common>,
            the_use_obb: bool,
        );
        pub fn SetGlue(self: Pin<&mut BRepAlgoAPI_Common>, glue: BOPAlgo_GlueEnum);
/* ... */
}

crates/opencascade/src/primitives/shape.rs

use crate::angle::Angle;
/* ... before/after */
        // let mut fuse_operation = ffi::BRepAlgoAPI_Common_ctor(&self.inner, &other.inner);
        let mut fuse_operation = ffi::BRepAlgoAPI_Common_ctor2(&self.inner, &other.inner);
/* ... */
impl Shape {
  pub fn intersect_with_params(
          &self,
          other: &Shape,
          parallel: bool,
          fuzzy: f64,
          obb: bool,
          glue: u8,
    ) -> BooleanShape {
        let mut common_operation = ffi::BRepAlgoAPI_Common_ctor();

        // set tools
        let mut tools = ffi::new_list_of_shape();
        ffi::shape_list_append_shape(tools.pin_mut(), &self.inner);
        common_operation.pin_mut().SetTools(&tools);

        // set arguments
        let mut arguments = ffi::new_list_of_shape();
        ffi::shape_list_append_shape(arguments.pin_mut(), &other.inner);
        common_operation.pin_mut().SetArguments(&arguments);

        // set additional options
        ffi::SetFuzzyValue_BRepAlgoAPI_Common(common_operation.pin_mut(), fuzzy);
        ffi::SetRunParallel_BRepAlgoAPI_Common(common_operation.pin_mut(), parallel);
        ffi::SetUseOBB_BRepAlgoAPI_Common(common_operation.pin_mut(), obb);
        match glue {
            2 => common_operation.pin_mut().SetGlue(ffi::BOPAlgo_GlueEnum::BOPAlgo_GlueFull),
            1 => common_operation.pin_mut().SetGlue(ffi::BOPAlgo_GlueEnum::BOPAlgo_GlueShift),
            _ => common_operation.pin_mut().SetGlue(ffi::BOPAlgo_GlueEnum::BOPAlgo_GlueOff),
        }

        // perform operation
        common_operation.pin_mut().Build(&ffi::Message_ProgressRange_ctor());

        // if ffi::HasErrors_BRepAlgoAPI_Common(&common_operation) {
        //     panic!("something went wrong");
        // }

        // get result edges
        let edge_list = common_operation.pin_mut().SectionEdges();
        let vec = ffi::shape_list_to_vector(edge_list);
        let mut new_edges = vec![];
        for shape in vec.iter() {
            let edge = ffi::TopoDS_cast_to_edge(shape);
            new_edges.push(Edge::from_edge(edge));
        }

        // get result shape
        let shape = Self::from_shape(common_operation.pin_mut().Shape());

        BooleanShape { shape, new_edges }
    }

    pub fn rotate(mut self, rotation_axis: DVec3, angle: Angle) -> Self {
        // create general transformation object
        let mut transform = ffi::new_transform();

        // apply rotation to transformation
        let rotation_axis_vec =
            ffi::gp_Ax1_ctor(&make_point(DVec3::ZERO), &make_dir(rotation_axis));
        transform.pin_mut().SetRotation(&rotation_axis_vec, angle.radians());

        // get result location
        let location = ffi::TopLoc_Location_from_transform(&transform);

        // apply transformation to shape
        self.inner.pin_mut().translate(&location, false);
        self
    }
/* ... */
}
/* ... */

crates/opencascade/src/primitives/solid.rs

/* ... before/after */
        // let mut fuse_operation = ffi::BRepAlgoAPI_Common_ctor(inner_shape, other_inner_shape);
        let mut fuse_operation = ffi::BRepAlgoAPI_Common_ctor2(inner_shape, other_inner_shape);
/* ... */
impl Solid {
    pub fn volume<'a, T>(
        shells_as_shapes: impl IntoIterator<Item = &'a T>,
        _avoid_internal_shapes: bool,
    ) -> Self
    where
        T: AsRef<Shape> + 'a,
    {
        // create Volume maker
        let mut maker = ffi::BOPAlgo_MakerVolume_ctor();

        // set shells to make solid from
        let mut arguments = ffi::new_list_of_shape();
        for shape in shells_as_shapes {
            ffi::shape_list_append_shape(arguments.pin_mut(), &shape.as_ref().inner);
        }
        maker.pin_mut().SetArguments(&arguments);

        // perform the opearation
        maker.pin_mut().Perform(&ffi::Message_ProgressRange_ctor());
        // cast result to solid according to doc
        let genaral_shape = ffi::BOPAlgo_MakerVolume_Shape(&maker);
        let solid = ffi::TopoDS_cast_to_solid(genaral_shape);
        Solid::from_solid(solid)
    }
/* ... */
}
novartole commented 1 month ago

Useful links for further research:

novartole commented 1 month ago

Current research became the basis for the repo.