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
  1. Rotate a single vertex around the +Y axis to generate a circular wire at the chosen radius.
  2. Seal that wire into a planar disk using try_attach_plane.
  3. 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
  1. Construct two symmetric arcs across the body width (arc0, and arc1 rotated 180°).
  2. Loft between the arcs using homotopy to generate a smooth side surface.
  3. 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
  1. Select the top face of the body (body_ceiling) and the rim wire from the neck shell.
  2. Insert the rim as an inner boundary to cut the neck opening in the ceiling face.
  3. 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
  1. Construct the outer body and attach the neck shell.
  2. Create a slightly smaller inner body and neck, attach them, then invert their faces.
  3. Move the inner rim to the outer ceiling as a boundary hole and stitch in the inner shell.
  4. 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();
}