Warning
Work in Progress! Feel free to try it out, fork and mod it for your use case. You are welcome to contribute your changes upstream. I will be glad to review and merge them if they fit the vision/scope of the project. Would love any feedback and help!
Add the dependency in your project's Cargo.toml
. Make sure you're using the right version tag. Refer to the Bevy Compatibility table.
[dependencies]
bevy_pmetra = { git = "https://github.com/nilaysavant/bevy_pmetra", tag = "v0.2.0" }
Refer to examples/simple_cube.rs
for full example.
Create struct for the SimpleCube
with fields being the parameters:
#[derive(Debug, Reflect, Component, Clone)]
struct SimpleCube {
/// Length of the side of the cube.
side_length: f64,
/// Number of cubes to spawn.
array_count: u32,
}
- Make sure to derive
Component
. As it will be added to the root/parentSpatialBundle
entity of the parametric model. Also deriveClone
. - Optionally derive
Reflect
andDebug
.
Implement Default
for default values of parameters:
impl Default for SimpleCube {
fn default() -> Self {
Self {
side_length: 0.5,
array_count: 1,
}
}
}
Implement a few traits
on our SimpleCube
struct for parametric behavior:
PmetraCad
: For generating multipleCadShell
(s) using this struct via Truck's modelling APIs.CadShell
is a wrapper around Truck'sShell
.PmetraModelling
: For parametrically generatingMesh
(s) fromCadShell
(s).PmetraInteractions
: (Optional) Setup interactions for live manipulations on models usingCadSlider
(s).
impl PmetraCad for SimpleCube {
fn shells_builders(&self) -> Result<CadShellsBuilders<Self>> {
CadShellsBuilders::new(self.clone())?
.add_shell_builder(CadShellName("SimpleCube".into()), cube_shell_builder)
}
}
CadShellsBuilders
lets us add multipleCadShell
builders per parametric model. Each builder is added as a callback function.- Since we only have a single kind of geometry/mesh we just need to add one shell builder for our cube:
cube_shell_builder
. This will need to return aCadShell
for our cube. We give it a name"SimpleCube"
which we can reference later. - If we need another kind of geometry/mesh (eg. cylinder, rectangle etc) we can add more such builders which will generate their equivalent shells. NB: We can only use the parameters of our
SimpleCube
struct to generate all theCadShell
(s).
Here is the code for cube_shell_builder
:
fn cube_shell_builder(params: &SimpleCube) -> Result<CadShell> {
let SimpleCube { side_length, .. } = ¶ms;
let mut tagged_elements = CadTaggedElements::default();
let vertex = Vertex::new(Point3::new(-side_length / 2., 0., side_length / 2.));
let edge = builder::tsweep(&vertex, Vector3::unit_x() * *side_length);
let face = builder::tsweep(&edge, -Vector3::unit_z() * *side_length);
tagged_elements.insert(
CadElementTag("ProfileFace".into()),
CadElement::Face(face.clone()),
);
let solid = builder::tsweep(&face, Vector3::unit_y() * *side_length);
let shell = Shell::try_from_solid(&solid)?;
Ok(CadShell {
shell,
tagged_elements,
})
}
- We model the cube using Truck's APIs. Refer Truck Cube Modelling Tutorial.
- We additionally tag the
"ProfileFace"
. This helps with positioning/orienting things likeCadSliders
with respect to the tagged element, as we will see later. - We can tag Truck's primitives like
Vertex
,Edge
,Wire
,Face
etc.
impl PmetraModelling for SimpleCube {
fn meshes_builders_by_shell(
&self,
shells_by_name: &CadShellsByName,
) -> Result<CadMeshesBuildersByCadShell<Self>> {
let mut meshes_builders_by_shell =
CadMeshesBuildersByCadShell::new(self.clone(), shells_by_name.clone())?;
let shell_name = CadShellName("SimpleCube".into());
for i in 0..self.array_count {
meshes_builders_by_shell.add_mesh_builder_with_outlines(
shell_name.clone(),
"SimpleCube".to_string() + &i.to_string(),
CadMeshBuilder::new(self.clone(), shell_name.clone())? // builder
.set_transform(Transform::from_translation(
Vec3::X * (i as f32 * (self.side_length as f32 * 1.5)),
))?
.set_base_material(Color::RED.into())?,
)?;
}
Ok(meshes_builders_by_shell)
}
}
CadShellsByName
holds theCadShell
(s) by the given name. In our case we will have a shell for"SimpleCube"
name.CadMeshesBuildersByCadShell
is used generate multipleMesh
(s) for each definedCadShell
via aCadMeshBuilder
.- We add a new mesh builder for our cube using
add_mesh_builder_with_outlines()
, which includes adding outlines for the generated meshes. We can useadd_mesh_builder()
for no outlines (more performance!). - To the above we pass the
shell_name
, a name for the mesh we will be generating, along with the builder for the same. - The
CadMeshBuilder
takes the parameter struct and theshell_name
. We can set theTransform
and theMaterial
of our mesh here. - Since we want to array the cubes (using
array_count
), we run this inside a for loop passing down the index (for naming) and also set the transform for each cube.
Tip
If you do not need interactions you can skip the PmetraInteractions section and jump to the Plugins section. With this you can already have parametric behavior. Just query for your parametric struct as a component (eg. SimpleCube
), and adjust its parameters!
Optionally you can implement this trait
for interactive sliders that can manipulate parameters of the SimpleCube
:
impl PmetraInteractions for SimpleCube {
fn sliders(&self, shells_by_name: &CadShellsByName) -> Result<CadSliders> {
let sliders = CadSliders::default() // sliders
.add_slider(
CadSliderName("SideLengthSlider".into()),
build_side_length_slider(self, shells_by_name)?,
)?
.add_slider(
CadSliderName("ArrayCountSlider".into()),
build_array_count_slider(self, shells_by_name)?,
)?;
Ok(sliders)
}
fn on_slider_transform(
&mut self,
name: CadSliderName,
prev_transform: Transform,
new_transform: Transform,
) {
if name.0 == "SideLengthSlider" {
let delta = new_transform.translation - prev_transform.translation;
if delta.length() > 0. {
self.side_length += delta.z as f64;
}
} else if name.0 == "ArrayCountSlider" {
let delta = new_transform.translation - prev_transform.translation;
if delta.length() > 0. {
self.array_count = (new_transform.translation.x / (self.side_length as f32 * 1.5))
.floor() as u32
+ 1;
}
}
}
fn on_slider_tooltip(&self, name: CadSliderName) -> Result<Option<String>> {
if name.0 == "SideLengthSlider" {
Ok(Some(format!("side_length: {:.2}", self.side_length)))
} else if name.0 == "ArrayCountSlider" {
Ok(Some(format!("array_count: {}", self.array_count)))
} else {
Ok(None)
}
}
}
- We configure the sliders using the
CadSliders
builder insliders()
using the receivedshells_by_name
. - Each slider can be added using
add_slider()
which accepts the name of the slider (for future reference/ID) and theCadSlider
struct itself. - Here we use utility functions to create the
CadSlider
struct likebuild_side_length_slider
for"SideLengthSlider"
. on_slider_transform
is called by the plugin whenever a slider's transform is changed. We receive theprev_transform
and thenew_transform
using which can change the parameters of ourSimpleCube
struct. The name of the slider is useful to distinguish and apply changes from the correct slider.on_slider_tooltip
is used to (optionally) set the tooltip text for the active slider.
Here is the code for build_side_length_slider
:
fn build_side_length_slider(
params: &SimpleCube,
shells_by_name: &CadShellsByName,
) -> Result<CadSlider> {
let SimpleCube { side_length, .. } = ¶ms;
let cad_shell = shells_by_name
.get(&CadShellName("SimpleCube".to_string()))
.ok_or_else(|| anyhow!("Could not get cube shell!"))?;
let Some(CadElement::Face(face)) =
cad_shell.get_element_by_tag(CadElementTag::new("ProfileFace"))
else {
return Err(anyhow!("Could not find face!"));
};
let face_normal = face.oriented_surface().normal(0.5, 0.5).as_bevy_vec3();
let face_boundaries = face.boundaries();
let face_wire = face_boundaries.last().expect("No wire found!");
let face_centroid = face_wire.get_centroid();
let slider_pos = face_centroid.as_vec3() + Vec3::Z * (*side_length as f32 / 2. + 0.1);
let slider_transform = Transform::from_translation(slider_pos)
.with_rotation(get_rotation_from_normals(Vec3::Z, face_normal));
Ok(CadSlider {
drag_plane_normal: face_normal,
transform: slider_transform,
slider_type: CadSliderType::Linear {
direction: Vec3::Z,
limit_min: Some(Vec3::Z * 0.2),
limit_max: Some(Vec3::INFINITY),
},
..default()
})
}
- We used the
"ProfileFace"
tag (we added earlier) to calculate the slider'sTransform
and also set the normal of the drag plane. - 2 types of sliders supported:
Linear
andPlaner
.Linear
also allows setting the drag limits of the slider along the given direction.
Now you can add the Pmetra Plugins to your Bevy App:
App::new() // app
.add_plugins((
PmetraBasePlugin::default(), // Base plugin
PmetraModellingPlugin::<SimpleCube>::default(),
PmetraInteractionsPlugin::<SimpleCube>::default(), // Optional
))
PmetraBasePlugin
is required and needs to be added only once per app.PmetraModellingPlugin
is required to be added for each parametricstruct
.SimpleCube
in this case.PmetraInteractionsPlugin
can be optionally added for the interactive sliders.
Now you can spawn the SimpleCube
model by firing an Event
:
fn spawn_simple_cube_model(mut spawn_simple_cube: EventWriter<GenerateCadModel<SimpleCube>>) {
spawn_simple_cube.send(GenerateCadModel::default());
}
Thats it! You can now see the magic:
Tip
For more sophisticated examples, checkout the models in the demo:
pmetra_demo/src/utils/cad_models
bevy | bevy_pmetra |
---|---|
0.14 | master , v0.2.0 |
0.13 | v0.1.0 |