Closed novartole closed 1 month 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.
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.
Life cycle of C++ objects.
Custom structures (smart pointers) of cxx are greatly described in documentation.
Class method creates and returns an instance of another class / casting to another type: The way how cxx works doesn't allow to pop-out instances from C++ to Rust directly. It's fine for most use cases, but sometimes an additional layer of indirection should be used.
Constructor call: A static function is required to create a class instance. This instance have to be wrapped into a smart pointer (usually _sharedptr or _uniqueptr). To make it less verbose, the power of C++ templates is used.
Method call: cxx doesn't allow to bind Pin<&mut Self> to a const method. Same for shared ref in Rust - &Self cannot be bind to a non-const method at C++ side.
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:
core of the crate
I would call them utils:
How to optimize operations (Boolean, Splitter, etc.)?
Implementation of boolean operations:
There are two libraries providing Boolean Operations:
Boolean operation algorithm in OCCT provides a self-diagnostic feature which can help to do that step. This feature can be activated by defining environment variable _CSF_DEBUGBOP, which should specify an existing writeable directory. The diagnostic code checks validity of the input arguments and the result of each Boolean operation. When an invalid situation is detected, the report consisting of argument shapes and a DRAW script to reproduce the problematic operation is saved to the directory pointed by _CSF_DEBUGBOP.
Useful links:
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):
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.
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:
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.
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.
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!
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.
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.
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 = []
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)
}
/* ... */
}
Useful links for further research: