Lofting interpolates between cross-sectional profiles placed at different heights to create smooth, organic shapes. This recipe builds a bottle by lofting through four circular profiles of varying radius.
The Profiles
The bottle's silhouette is defined by four circles stacked along Z. Each circle sits at a different height and has a different radius, giving the bottle its characteristic shape -- wide body tapering to a narrow neck.
use vcad::{Sketch, Loft, Part};
// Define cross-section profiles at different heights
let base = Sketch::circle(30.0).at_z(0.0); // Flat base
let body = Sketch::circle(35.0).at_z(40.0); // Widest point
let shoulder = Sketch::circle(15.0).at_z(80.0); // Taper inward
let neck = Sketch::circle(10.0).at_z(100.0); // Narrow neck
let bottle = Loft::through(&[base, body, shoulder, neck])
.segments(64)
.build();
The base profile (r=30mm) sits at Z=0. Moving upward, the body profile (r=35mm) at Z=40 creates a gentle outward swell. The shoulder at Z=80 pulls sharply inward to r=15mm, and the neck finishes at r=10mm and Z=100mm. The loft engine generates a smooth surface that passes through all four sections.
Step by Step
1. Sketch the cross-sections
Each profile is a simple circle, but you can use any closed sketch shape -- ellipses, rounded rectangles, or freeform curves all work with the loft operation.
let base = Sketch::circle(30.0).at_z(0.0);
let body = Sketch::circle(35.0).at_z(40.0);
let shoulder = Sketch::circle(15.0).at_z(80.0);
let neck = Sketch::circle(10.0).at_z(100.0);
The .at_z() method positions each sketch plane along the Z axis. Since vcad uses Z-up coordinates, the bottle grows vertically from base to neck.
2. Loft between them
let bottle = Loft::through(&[base, body, shoulder, neck])
.segments(64)
.build();
The loft operation generates ruled or smoothly interpolated surfaces between consecutive profile pairs. With 64 circumferential segments, the result is visually smooth and suitable for STL export. The loft respects the profile ordering -- profiles must be listed from bottom to top (ascending Z).
3. Hollow the bottle
A real bottle needs a cavity. Shell the solid to create uniform wall thickness, leaving the top face open:
let hollow = bottle.shell(2.0, ShellOpen::Top);
This removes material from the interior, leaving 2mm walls everywhere except the top face (the neck opening). If you want the bottom face left open instead, use ShellOpen::Bottom.
4. Add a lip
The neck needs a rim for a cap or stopper:
let lip = centered_cylinder("lip", 12.0, 5.0, 64)
.translate(0.0, 0.0, 100.0);
let bottle_with_lip = hollow + lip;
The lip cylinder is slightly larger than the neck radius (12mm vs 10mm), creating a 2mm protruding rim at the top. This provides a sealing surface for a cap.
Controlling the Loft Shape
Tangent continuity
By default, loft uses linear interpolation between profiles, which can produce visible creases at profile boundaries. For a smoother surface, enable tangent continuity:
let smooth = Loft::through(&[base, body, shoulder, neck])
.segments(64)
.continuity(LoftContinuity::Tangent)
.build();
Tangent continuity ensures the surface normal changes smoothly across profile transitions, eliminating visible seams in the rendered result.
Adding intermediate profiles
More profiles give you finer control over the shape. Adding a profile between body and shoulder lets you shape the transition:
let mid = Sketch::circle(28.0).at_z(60.0); // Gradual taper
let refined = Loft::through(&[base, body, mid, shoulder, neck])
.segments(64)
.build();
Non-circular profiles
The bottle body can use a rounded rectangle for a flat-sided design:
let flat_body = Sketch::rounded_rect(50.0, 40.0, 5.0).at_z(40.0);
let flat_shoulder = Sketch::rounded_rect(25.0, 20.0, 4.0).at_z(80.0);
let flat_bottle = Loft::through(&[base, flat_body, flat_shoulder, neck])
.segments(64)
.build();
When lofting between different profile shapes (circle to rounded rectangle), the loft engine morphs the cross-section smoothly. The result is a bottle with flat sides that transitions to a round neck.
All profiles in a loft must have the same number of segments or the loft engine will resample them to match. For best results when mixing shapes, use the same segment count for all profiles.
Complete Code
use vcad::{centered_cylinder, Sketch, Loft, Part, ShellOpen};
fn bottle(
base_r: f64,
body_r: f64,
shoulder_r: f64,
neck_r: f64,
body_height: f64,
shoulder_height: f64,
total_height: f64,
wall: f64,
) -> Part {
let base = Sketch::circle(base_r).at_z(0.0);
let body = Sketch::circle(body_r).at_z(body_height);
let shoulder = Sketch::circle(shoulder_r).at_z(shoulder_height);
let neck = Sketch::circle(neck_r).at_z(total_height);
let solid = Loft::through(&[base, body, shoulder, neck])
.segments(64)
.continuity(LoftContinuity::Tangent)
.build();
// Hollow with open top
let hollow = solid.shell(wall, ShellOpen::Top);
// Add neck lip
let lip = centered_cylinder("lip", neck_r + wall, 3.0, 64)
.translate(0.0, 0.0, total_height);
hollow + lip
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let part = bottle(
30.0, // base radius
35.0, // body radius (widest)
15.0, // shoulder radius
10.0, // neck radius
40.0, // body height
80.0, // shoulder height
100.0, // total height
2.0, // wall thickness
);
println!("Volume: {:.0} mm³", part.volume());
part.write_stl("bottle.stl")?;
part.write_step("bottle.step")?;
Ok(())
}
Export to STEP for CNC or injection mold tooling. The lofted BRep surfaces export as exact geometry (B-spline surfaces), not tessellated approximations. This preserves the smooth curvature data that manufacturing tools need.
Variations
Vase with wavy profile
Alternating wide and narrow profiles creates a decorative wave pattern:
let profiles = [
Sketch::circle(25.0).at_z(0.0),
Sketch::circle(30.0).at_z(20.0),
Sketch::circle(22.0).at_z(40.0),
Sketch::circle(28.0).at_z(60.0),
Sketch::circle(20.0).at_z(80.0),
];
let vase = Loft::through(&profiles)
.segments(64)
.build()
.shell(2.0, ShellOpen::Top);
Tapered flask
Two wide profiles in the middle with narrow top and bottom creates a flask shape:
let flask = Loft::through(&[
Sketch::circle(8.0).at_z(0.0),
Sketch::circle(30.0).at_z(15.0),
Sketch::circle(30.0).at_z(60.0),
Sketch::circle(8.0).at_z(75.0),
])
.segments(64)
.build();