Higher Level Modeling (Bottle)
Model a bottle using Truck’s lower-level B-rep APIs (inspired by the classic OCCT bottle tutorial), keeping geometry and exports in the library just like the cube/torus/cylinder pages.
Build a bottle with arcs, homotopy, sweeps, and glue ops
- Spin + cap: rotational sweep for neck circles, then cap with
try_attach_plane - Side loft: arcs →
homotopy→ swept body shell - Glue: punch ceiling hole and stitch neck shell onto body
- Hollowing: offset inner shell, invert faces, sew into outer body
src/bottle.rs
Keep helpers in src/lib.rs, and the bottle shape in its own file (sibling to lib.rs).
#![allow(unused)] fn main() { use std::f64::consts::PI; use truck_modeling::builder; use truck_modeling::*; }
1. Neck shell helper (cylinder)
#![allow(unused)] fn main() { /// Shell for the neck or inner neck. pub fn cylinder(bottom: f64, height: f64, radius: f64) -> Shell { let vertex = builder::vertex(Point3::new(0.0, bottom, radius)); let circle = builder::rsweep(&vertex, Point3::origin(), Vector3::unit_y(), Rad(7.0)); let disk = builder::try_attach_plane(&vec![circle]).unwrap(); let solid = builder::tsweep(&disk, Vector3::new(0.0, height, 0.0)); solid.into_boundaries().pop().unwrap() // extract shell } }
How cylinder() works
- Rotate a single vertex around the +Y axis to generate a circular wire at the chosen radius.
- Seal that wire into a planar disk using
try_attach_plane. - Sweep the disk upward along +Y to form a solid, then extract its outer shell.
2. Body shell helper (body_shell)
#![allow(unused)] fn main() { /// Outer/inner body shell built from arcs, homotopy, and sweep. pub fn body_shell(bottom: f64, height: f64, width: f64, thickness: f64) -> Shell { let v0 = builder::vertex(Point3::new(-width / 2.0, bottom, thickness / 4.0)); let v1 = builder::vertex(Point3::new(width / 2.0, bottom, thickness / 4.0)); let transit = Point3::new(0.0, bottom, thickness / 2.0); let arc0 = builder::circle_arc(&v0, &v1, transit); let arc1 = builder::rotated(&arc0, Point3::origin(), Vector3::unit_y(), Rad(PI)); let face = builder::homotopy(&arc0, &arc1.inverse()); let solid = builder::tsweep(&face, Vector3::new(0.0, height, 0.0)); solid.into_boundaries().pop().unwrap() } }
How body_shell() works
- Construct two symmetric arcs across the body width (
arc0, andarc1rotated 180°). - Loft between the arcs using
homotopyto generate a smooth side surface. - Sweep that surface upward along +Y to create the body shell, then extract it.
3. Glue neck onto body (glue_body_neck)
#![allow(unused)] fn main() { /// Punch a hole in the ceiling and glue neck faces on top. pub fn glue_body_neck(body: &mut Shell, neck: Shell) { let body_ceiling = body.last_mut().unwrap(); let wire = neck[0].boundaries()[0].clone(); // This boundary punch is the ceiling hole for the neck. body_ceiling.add_boundary(wire); // punch hole for neck body.extend(neck.into_iter().skip(1)); // add remaining neck faces } }
How glue_body_neck() works
- Select the top face of the body (
body_ceiling) and the rim wire from the neck shell. - Insert the rim as an inner boundary to cut the neck opening in the ceiling face.
- Attach the remaining neck faces to the body shell.
4. Assemble the full bottle (bottle)
#![allow(unused)] fn main() { /// Hollow bottle with inner cavity and neck. pub fn bottle(height: f64, width: f64, thickness: f64) -> Solid { let mut body = body_shell(-height / 2.0, height, width, thickness); let neck = cylinder(height / 2.0, height / 10.0, thickness / 4.0); glue_body_neck(&mut body, neck); let eps = height / 50.0; // Inner shell (shrunk and inset) let mut inner_body = body_shell( -height / 2.0 + eps, height - 2.0 * eps, width - 2.0 * eps, thickness - 2.0 * eps, ); let inner_neck = cylinder( height / 2.0 - eps, height / 10.0 + eps, thickness / 4.0 - eps, ); glue_body_neck(&mut inner_body, inner_neck); // Flip inner faces inward and sew into outer shell inner_body.face_iter_mut().for_each(|face| face.invert()); let inner_ceiling = inner_body.pop().unwrap(); let wire = inner_ceiling.into_boundaries().pop().unwrap(); let ceiling = body.last_mut().unwrap(); ceiling.add_boundary(wire); body.extend(inner_body.into_iter()); Solid::new(vec![body]) } }
How bottle() works
- Construct the outer body and attach the neck shell.
- Create a slightly smaller inner body and neck, attach them, then invert their faces.
- Move the inner rim to the outer ceiling as a boundary hole and stitch in the inner shell.
- Package the resulting shell hierarchy into a
Solid.
Update src/lib.rs to expose bottle
#![allow(unused)] fn main() { pub mod bottle; pub use bottle::bottle; }
Example entry point
examples/bottle.rs just calls into the library:
fn main() { let bottle = truck_brep::bottle(2.0, 1.0, 0.6); truck_brep::save_obj(&bottle, "output/bottle.obj").unwrap(); truck_brep::save_step(&bottle, "output/bottle.step").unwrap(); }
Directory tree (for this section)
truck_brep/
├─ src/
│ ├─ lib.rs # helpers + re-exports
│ ├─ cube.rs
│ ├─ torus.rs
│ ├─ cylinder.rs
│ └─ bottle.rs # this section
├─ examples/
│ ├─ cube.rs
│ ├─ torus.rs
│ ├─ cylinder.rs
│ └─ bottle.rs
└─ output/ # created at runtime
Complete code
src/lib.rs
#![allow(unused)] fn main() { use std::{fs, io, path::Path}; use truck_meshalgo::prelude::*; use truck_modeling::*; use truck_stepio::out::{CompleteStepDisplay, StepModel}; use truck_topology::compress::{CompressedShell, CompressedSolid}; pub mod cube; pub use cube::cube; pub mod torus; pub use torus::torus; pub mod cylinder; pub use cylinder::cylinder; pub mod bottle; pub use bottle::bottle; /// Helper to compress modeling shapes into STEP-compatible data. pub trait StepCompress { type Compressed; fn compress_for_step(&self) -> Self::Compressed; } impl StepCompress for Shell { type Compressed = CompressedShell<Point3, Curve, Surface>; fn compress_for_step(&self) -> Self::Compressed { self.compress() } } impl StepCompress for Solid { type Compressed = CompressedSolid<Point3, Curve, Surface>; fn compress_for_step(&self) -> Self::Compressed { self.compress() } } /// Export any B-rep (Solid or Shell) to STEP. pub fn save_step<T, P>(brep: &T, path: P) -> io::Result<()> where T: StepCompress, for<'a> StepModel<'a, Point3, Curve, Surface>: From<&'a T::Compressed>, P: AsRef<Path>, { let path = path.as_ref(); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let compressed = brep.compress_for_step(); let display = CompleteStepDisplay::new( StepModel::from(&compressed), Default::default(), ); fs::write(path, display.to_string()) } /// Triangulate any B-rep (Solid or Shell) and write an OBJ mesh. pub fn save_obj(shape: &impl MeshableShape, path: impl AsRef<Path>) -> io::Result<()> { let mesh = shape.triangulation(0.01).to_polygon(); let path = path.as_ref(); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } let mut obj = fs::File::create(path)?; obj::write(&mesh, &mut obj).map_err(|err| io::Error::new(io::ErrorKind::Other, err)) } }
src/bottle.rs
#![allow(unused)] fn main() { use std::f64::consts::PI; use truck_modeling::builder; use truck_modeling::*; pub fn cylinder(bottom: f64, height: f64, radius: f64) -> Shell { let vertex = builder::vertex(Point3::new(0.0, bottom, radius)); let circle = builder::rsweep(&vertex, Point3::origin(), Vector3::unit_y(), Rad(7.0)); let disk = builder::try_attach_plane(&vec![circle]).unwrap(); let solid = builder::tsweep(&disk, Vector3::new(0.0, height, 0.0)); solid.into_boundaries().pop().unwrap() } pub fn body_shell(bottom: f64, height: f64, width: f64, thickness: f64) -> Shell { let v0 = builder::vertex(Point3::new(-width / 2.0, bottom, thickness / 4.0)); let v1 = builder::vertex(Point3::new(width / 2.0, bottom, thickness / 4.0)); let transit = Point3::new(0.0, bottom, thickness / 2.0); let arc0 = builder::circle_arc(&v0, &v1, transit); let arc1 = builder::rotated(&arc0, Point3::origin(), Vector3::unit_y(), Rad(PI)); let face = builder::homotopy(&arc0, &arc1.inverse()); let solid = builder::tsweep(&face, Vector3::new(0.0, height, 0.0)); solid.into_boundaries().pop().unwrap() } /// Punch a hole in the ceiling and glue neck faces on top. pub fn glue_body_neck(body: &mut Shell, neck: Shell) { let body_ceiling = body.last_mut().unwrap(); let wire = neck[0].boundaries()[0].clone(); // This boundary punch is the ceiling hole for the neck. body_ceiling.add_boundary(wire); body.extend(neck.into_iter().skip(1)); } /// Hollow bottle with inner cavity and neck. pub fn bottle(height: f64, width: f64, thickness: f64) -> Solid { let mut body = body_shell(-height / 2.0, height, width, thickness); let neck = cylinder(height / 2.0, height / 10.0, thickness / 4.0); glue_body_neck(&mut body, neck); let eps = height / 50.0; let mut inner_body = body_shell( -height / 2.0 + eps, height - 2.0 * eps, width - 2.0 * eps, thickness - 2.0 * eps, ); let inner_neck = cylinder( height / 2.0 - eps, height / 10.0 + eps, thickness / 4.0 - eps, ); glue_body_neck(&mut inner_body, inner_neck); inner_body.face_iter_mut().for_each(|face| face.invert()); let inner_ceiling = inner_body.pop().unwrap(); let wire = inner_ceiling.into_boundaries().pop().unwrap(); let ceiling = body.last_mut().unwrap(); ceiling.add_boundary(wire); body.extend(inner_body.into_iter()); Solid::new(vec![body]) } }
examples/bottle.rs
fn main() { let bottle = truck_brep::bottle(2.0, 1.0, 0.6); truck_brep::save_obj(&bottle, "output/bottle.obj").unwrap(); truck_brep::save_step(&bottle, "output/bottle.step").unwrap(); }