csgrs 0.8.0

Constructive solid geometry on meshes using BSP trees in Rust
Documentation

csgrs

A Constructive Solid Geometry (CSG) library in Rust, built around Boolean operations on sets of polygons stored in BSP trees. This allows you to construct and manipulate 2D and 3D geometry with operations such as union, difference, intersection, and more—much like OpenSCAD does, but in Rust.

This library aims to integrate cleanly with the Dimforge ecosystem (e.g., nalgebra, Parry, and Rapier), leverage earclip and cavalier_contours for robust mesh and line processing, be reasonably performant on a wide variety of targets, and provide an extensible, type-safe API.

Table of Contents

  1. Features
  2. Installation
  3. Quick Start Example
  4. Library Overview
  5. File I/O
  6. Integration with Parry and Rapier
  7. Manifold Check
  8. Roadmap / Todo
  9. License

Features

  • BSP-based CSG boolean operations: union, difference, intersection.
  • 2D (XY-plane) polygons and advanced 2D booleans via cavalier_contours.
  • 3D shape construction: cubes, spheres, cylinders, polyhedrons from face lists, and more.
  • Transformations: translate, rotate, scale, mirror, etc.
  • Extrusions: linear extrude, rotate-extrude (revolve), extrude-between arbitrary polygons.
  • Triangulation (via [earclip]) and polygon refinement methods (subdivide, renormalize, etc.).
  • Optional concurrency with the "parallel" feature (uses rayon).
  • Optional interoperability with [Rapier] and [Parry] for physics, collisions, bounding volumes, etc.
  • Import/export from/to ASCII or binary STL, DXF, plus 2D text generation from TTF fonts.
  • Generic per-polygon metadata to store color, layer IDs, or any custom data.

Note: Some features (e.g. parallel operations, STL, DXF, Rapier integration) may eventually be placed behind feature flags.

Installation

Add the following to your Cargo.toml:

[dependencies]
csgrs = "^0.8.0"

Quick Start Example

use nalgebra::Vector3;
use csgrs::{CSG, Axis};

// Alias the library’s generic CSG type with empty metadata:
type MyCSG = CSG<()>;

// Create two shapes:
let cube = MyCSG::cube(None);       // 2×2×2 cube centered at origin
let sphere = MyCSG::sphere(None);   // sphere of radius=1 at origin

// Compute union:
let union_result = cube.union(&sphere);

// Write the result as an ASCII STL:
let stl_text = union_result.to_stl_ascii("cube_plus_sphere");
std::fs::write("cube_sphere_union.stl", stl_text).unwrap();

// For more advanced usage (e.g., rapier integration, 2D offsetting, etc.), see below.

Library Overview

CSG and Polygon Structures

  • CSG<S> is the main type. It stores a list of polygons (Vec<Polygon<S>>).
  • Polygon<S> holds:
    • a Vec<Vertex> (positions + normals),
    • an optional metadata field (Option<S>), and
    • a Plane describing the polygon’s orientation in 3D.

You can build a CSG<S> from polygons with CSG::from_polygons(...).

2D Shapes

Helper constructors for 2D shapes in the XY plane:

  • CSG::square(Some(([width, height], center)))
  • CSG::circle(Some((radius, segments)))
  • CSG::polygon_2d(&[[x1,y1],[x2,y2],...])

Examples:

let square = MyCSG::square(None);          // 1×1 at origin
let centered_rect = MyCSG::square(Some(([2.0, 4.0], true)));
let circle = MyCSG::circle(None);          // radius=1, 32 segments
let circle2 = MyCSG::circle(Some((2.0, 64)));

3D Shapes

Similarly, you can create standard 3D primitives:

  • CSG::cube(Some((&center, &radius)))
  • CSG::sphere(Some((&center, radius, slices, stacks)))
  • CSG::cylinder(Some((&start, &end, radius, slices)))
  • CSG::polyhedron(points, faces)

Examples:

// Unit cube at origin
let cube = MyCSG::cube(None);

// Sphere of radius=2 at origin with 32 slices and 16 stacks
let sphere = MyCSG::sphere(Some((&[0.0, 0.0, 0.0], 2.0, 32, 16)));

// Cylinder from (0, -1, 0) to (0, 1, 0) with radius=1 and 16 slices
let cyl = MyCSG::cylinder(Some((&[0.0, -1.0, 0.0], &[0.0, 1.0, 0.0], 1.0, 16)));

// Create a custom polyhedron from points and face indices:
let points = &[
    [0.0, 0.0, 0.0],
    [1.0, 0.0, 0.0],
    [1.0, 1.0, 0.0],
    [0.0, 1.0, 0.0],
    [0.5, 0.5, 1.0],
];
let faces = vec![
    vec![0, 1, 2, 3], // base rectangle
    vec![0, 1, 4],    // triangular side
    vec![1, 2, 4],
    vec![2, 3, 4],
    vec![3, 0, 4],
];
let pyramid = MyCSG::polyhedron(points, &faces);

Boolean Operations

Three primary operations:

  1. Union: a.union(&b)
  2. Difference: a.subtract(&b)
  3. Intersection: a.intersect(&b)

They all return a new CSG<S>.

let union_result = cube.union(&sphere);
let subtraction_result = cube.subtract(&sphere);
let intersection_result = cylinder.intersect(&sphere);

Transformations

  • translate(v: Vector3<f64>)
  • rotate(x_deg, y_deg, z_deg)
  • scale(sx, sy, sz)
  • mirror(Axis::X | Axis::Y | Axis::Z)
  • transform(&Matrix4<f64>) for arbitrary affine transforms.
use nalgebra::Vector3;

let moved = cube.translate(Vector3::new(3.0, 0.0, 0.0));
let rotated = sphere.rotate(0.0, 45.0, 90.0);
let scaled = cylinder.scale(2.0, 1.0, 1.0);
let mirrored = cube.mirror(Axis::Z);

Extrusions and Revolves

  • Linear Extrude:
    • my_2d_shape.extrude(height: f64)
    • my_2d_shape.extrude_vector(direction: Vector3<f64>)
  • Extrude Between Two Polygons:
    let polygon_bottom = MyCSG::circle(Some((2.0, 64)));
    let polygon_top = polygon_bottom.translate(Vector3::new(0.0, 0.0, 5.0));
    let lofted = MyCSG::extrude_between(&polygon_bottom.polygons[0],
                                        &polygon_top.polygons[0],
                                        false);
    
  • Rotate-Extrude (Revolve): my_2d_shape.rotate_extrude(angle_degs, segments)
let square = MyCSG::square(Some(([2.0,2.0], false)));
let prism = square.extrude(5.0);

let revolve_shape = square.rotate_extrude(360.0, 16);

Miscellaneous Operations

  • CSG::inverse() — flips the inside/outside orientation.
  • CSG::convex_hull() — uses chull to generate a 3D convex hull.
  • CSG::minkowski_sum(&other) — naive Minkowski sum, then takes the hull.
  • CSG::ray_intersections(origin, direction) — returns all intersection points and distances.
  • CSG::flatten() — flattens a 3D shape into 2D (on the XY plane), unions the outlines.
  • CSG::cut(Some(plane)) — slices the CSG by a plane and returns the cross-section polygons.
  • CSG::offset_2d(distance) — outward (or inward) offset in 2D using [cavalier_contours].
  • CSG::grow(distance), CSG::shrink(distance) (3D offset, currently approximate/experimental).
  • CSG::subdivide_triangles(levels) — subdivides each polygon’s triangles, increasing mesh density.
  • CSG::renormalize() — re-computes each polygon’s plane from its vertices, resetting all normals.

Working with Metadata

CSG<S> is generic over S: Clone. Each polygon has an optional metadata: Option<S>.
Use cases include storing color, ID, or layer info.

#[derive(Clone)]
struct MyMetadata {
    color: (u8,u8,u8),
    label: String,
}

type MyCSG = CSG<MyMetadata>;

// For a single polygon:
use nalgebra::{Point3, Vector3};
use csgrs::{Polygon, Vertex, Plane};

let mut poly = Polygon::new(
    vec![
        Vertex::new(Point3::new(0.0, 0.0, 0.0), Vector3::z()),
        Vertex::new(Point3::new(1.0, 0.0, 0.0), Vector3::z()),
        Vertex::new(Point3::new(0.0, 1.0, 0.0), Vector3::z()),
    ],
    Some(MyMetadata { color: (255,0,0), label: "Triangle".into() }),
);

// Retrieve metadata
if let Some(data) = poly.metadata() {
    println!("This polygon is labeled {}", data.label);
}

// Mutate metadata
if let Some(data_mut) = poly.metadata_mut() {
    data_mut.label.push_str("_extended");
}

File I/O

STL

  • Export ASCII STL: csg.to_stl_ascii("solid_name") -> String
  • Export Binary STL: csg.to_stl_binary("solid_name") -> io::Result<Vec<u8>>
  • Import STL: CSG::from_stl(&stl_data) -> io::Result<CSG<S>>
// Save to ASCII STL
let stl_text = csg_union.to_stl_ascii("union_solid");
std::fs::write("union_ascii.stl", stl_text).unwrap();

// Save to binary STL
let stl_bytes = csg_union.to_stl_binary("union_solid").unwrap();
std::fs::write("union_bin.stl", stl_bytes).unwrap();

// Load from an STL file on disk
let file_data = std::fs::read("some_file.stl")?;
let imported_csg = CSG::from_stl(&file_data)?;

DXF

  • Export: csg.to_dxf() -> Result<Vec<u8>, Box<dyn Error>>
  • Import: CSG::from_dxf(&dxf_data) -> Result<CSG<S>, Box<dyn Error>>
// Export DXF
let dxf_bytes = csg_obj.to_dxf()?;
std::fs::write("output.dxf", dxf_bytes)?;

// Import DXF
let dxf_data = std::fs::read("some_file.dxf")?;
let csg_dxf = CSG::from_dxf(&dxf_data)?;

2D Text

You can generate 2D text geometry in the XY plane from TTF fonts via meshtext:

let font_data = include_bytes!("../fonts/MyFont.ttf");
let csg_text = MyCSG::text("Hello!", font_data, Some(20.0));

// Then extrude the text to make it 3D:
let text_3d = csg_text.extrude(1.0);

Integration with Parry and Rapier

Create a Parry TriMesh

csg.to_trimesh() returns a SharedShape containing a TriMesh<f64>.

use csgrs::CSG;
use rapier3d_f64::prelude::*;

let trimesh_shape = csg_obj.to_trimesh(); // SharedShape with a TriMesh

Create a Rapier Rigid Body

csg.to_rigid_body(rb_set, co_set, translation, rotation, density) helps build and insert both a rigid body and a collider:

use nalgebra::Vector3;
use rapier3d_f64::prelude::*;
use csgrs::CSG;

let mut rb_set = RigidBodySet::new();
let mut co_set = ColliderSet::new();

let axis_angle = Vector3::z() * std::f64::consts::FRAC_PI_2; // 90° around Z
let rb_handle = csg_obj.to_rigid_body(
    &mut rb_set,
    &mut co_set,
    Vector3::new(0.0, 0.0, 0.0), // translation
    axis_angle,                  // axis-angle
    1.0,                         // density
);

Mass Properties

let density = 1.0;
let (mass, com, inertia_frame) = csg_obj.mass_properties(density);
println!("Mass: {}", mass);
println!("Center of Mass: {:?}", com);
println!("Inertia local frame: {:?}", inertia_frame);

Manifold Check

csg.is_manifold() performs a quick check by writing a temporary binary STL in memory, reading it back into an indexed mesh, and validating that mesh. Returns Ok(true) if manifold, Ok(false) if not, or an Err on I/O issues.

match csg_obj.is_manifold()? {
    true => println!("CSG is manifold!"),
    false => println!("Not manifold."),
}

Roadmap / Todo

Todo maybe

  • implement arc support in 2d using cavalier_contours, interpolate/tessellate in from_polygons
  • reconstruct arcs from polylines using
  • extend Polygon to allow edges to store arc parameters and bulge like cavalier_contours and update split_polygon to handle line/arc intersections.

License

MIT License

Copyright (c) 2025 Timothy Schmidt

Permission is hereby granted, free of charge, to any person obtaining a copy of this 
software and associated documentation files (the "Software"), to deal in the Software 
without restriction, including without limitation the rights to use, copy, modify, merge, 
publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons 
to whom the Software is furnished to do so, subject to the following conditions:

[... full MIT license text ...]

This library initially based on a translation of CSG.js © 2011 Evan Wallace, under the MIT license.


Enjoy building geometry in Rust! If you find issues, please file an issue or submit a pull request. Feedback and contributions are welcome!