## Saturday, August 25, 2012

### Python Constructive Solid Geometry Update

In a earlier posts I've alluded to a Python Constructive Solid Geometry (CSG) library that I was working on to allow parametric design.  You can do this with OpenSCAD, which is great software, but in my opinion the language leaves a bit to be desired. I wanted a solution that worked with existing languages, specifically C, C++ and Python, so that the results could be integrated easily with other software such as remeshers or FEA packages.

Of course writing a robust CSG library is a daunting undertaking.  Fortunately there are existing libraries such as CGAL and Carve that handle this.  In my opinion CGAL is the more robust of the two, however it currently has compilation issues under OS-X and is substantially slower than Carve.

Regardless, neither have the interface that I'm looking for, like the ability to directly load meshes, affine transformations and minimal-code ways to perform boolean operations on meshes.  So I started work on a C++ wrapper for Carve that would give me the interface I wanted, with a wrapper for Python.

I'm pleased to say that it's coming along quite well and able to produce parts that are non-trivial.  The interface is considerably cleaned up from before and I'm now starting to use it for projects.  Here's two examples from (another) CNC project:

The code that generated these models is here:

from pyCSG import *

def inch_to_mm( inches ):
return inches*25.4

def mm_to_inch( mm ):
return mm/25.4

def hole_compensation( diameter ):
return diameter+1.0

mounting_hole_radius = 0.5*hole_compensation( inch_to_mm( 5.0/16.0 ) )

def axis_end():
obj = box( inch_to_mm( 4.5 ), inch_to_mm( 1.75 ), inch_to_mm( 0.75 ), True )

screw_hole = cylinder( mounting_hole_radius, inch_to_mm( 3.0 ), True, 20 )

shaft_hole = cylinder( 0.5*hole_compensation( inch_to_mm( 0.5 ) ), inch_to_mm(1.0), True, 20 ).rotate( 90.0, 0.0, 0.0 )

center_hole = cylinder( 0.5*hole_compensation( inch_to_mm( 1.0 ) ), inch_to_mm(1.0), True, 20 ).rotate( 90.0, 0.0, 0.0 )
mount_hole = cylinder( 0.5*hole_compensation( 4.0), inch_to_mm(1.0), True, 10 ).rotate( 90.0, 0.0, 0.0 )

notch = box( inch_to_mm( 1.5 ), 2.0, inch_to_mm( 1.0 ), True )

obj = obj - ( shaft_hole.translate( inch_to_mm( 1.5 ), 0.0, 0.0 ) + shaft_hole.translate( inch_to_mm( -1.5 ), 0.0, 0.0 ) )
obj = obj - ( notch.translate( inch_to_mm( 2.25 ), 0.0, 0.0 ) + notch.translate( inch_to_mm( -2.25 ), 0.0, 0.0 ) )
obj = obj - ( center_hole + mount_hole.translate( -15.5, -15.5, 0.0 ) + mount_hole.translate(  15.5, -15.5, 0.0 ) + mount_hole.translate( 15.5, 15.5, 0.0 ) + mount_hole.translate( -15.5, 15.5, 0.0 ) )

obj = obj - ( screw_hole.translate( inch_to_mm(1.0), 0.0, 0.0 ) + screw_hole.translate( inch_to_mm(-1.0), 0.0, 0.0 ) )
obj = obj - ( screw_hole.translate( inch_to_mm(2.0), 0.0, 0.0 ) + screw_hole.translate( inch_to_mm(-2.0), 0.0, 0.0 ) )

return obj

def carriage():
obj = box( inch_to_mm( 5 ), inch_to_mm( 5 ), inch_to_mm( 1.0 ), True )
shaft_hole = cylinder( inch_to_mm( 0.75 )/2.0, inch_to_mm( 5.5 ), True )
screw_hole = cylinder( inch_to_mm( 0.5 )/2.0, inch_to_mm( 5.5 ), True )

leadnut_hole = cylinder( inch_to_mm(0.25)*0.5, inch_to_mm( 1.0 ), True );
leadnut_access = box( inch_to_mm( 1.5 ), inch_to_mm( 3.0/8.0 ), inch_to_mm( 1.0 ), True )

mhole = cylinder( mounting_hole_radius, inch_to_mm( 2.0 ), True ).rotate( 90.0, 0.0, 0.0 )

obj = obj - ( shaft_hole.translate( inch_to_mm( 1.5 ), 0.0, 0.0 ) + shaft_hole.translate( inch_to_mm( -1.5 ), 0.0, 0.0 ) + screw_hole )
obj = obj - ( leadnut_hole.translate( inch_to_mm( 0.5 ), inch_to_mm( -2.5 ), 0.0 ) + leadnut_hole.translate( inch_to_mm( -0.5 ), inch_to_mm( -2.5 ), 0.0 ) + leadnut_access.translate( 0.0, inch_to_mm( -2.0 ), inch_to_mm( 0.2 ) ) )

for i in range( -2, 3 ):
for j in range( -2, 3 ):
if i != 0 and j != 0:
obj = obj - ( mhole.translate( inch_to_mm( 1.0*i ), inch_to_mm( 1.0*j ), 0.0 ) )
return obj

axis_end().save_mesh("axis_end.obj" )
carriage().save_mesh("carriage.obj" )


As you can see, this approach gives lots of flexibility in terms of manipulating and patterning objects using custom code.  The examples above are not great examples of parametric design, but I'm sure you can imagine the sort of stuff that can be done.

I still have to perform a bit of cleanup outside the library to get printable models.  I just run each model through MeshLab's planar edge-flipping optimizer. This is a pretty simple step and I plan to integrate it into the library shortly, along with the ability to extrude custom profiles and build surfaces of revolution.  When these features are finished I plan to release the code for the library and Python wrapper.