Skip to main content

OpenSCAD, with a Python Spice

Project description

MuSCAD: OpenSCAD with a Python spice

MuSCAD lets you write simple, readable, Pythonic code, that automatically translates into valid OpenSCAD code. It is loosely inspired by SolidPython, which is itself based on Phillip Tiefenbacher’s openscad module, found on Thingiverse.

Here’s a simple example. This simple Python code:

from muscad import Cube, Sphere, Cylinder
print(
    Cube(10, 10, 10).down(10)
  - Sphere(d=15)
  + Cylinder(h=15, d=12).leftward(16)
)

Will output this OpenSCAD code:

union() {
    difference() {
        translate(v=[0, 0, -10]) {
            cube(size=[10, 10, 10]);
        }
        sphere(d=15);
    }
    translate(v=[-16, 0, 0]) {
        cylinder(h=15, d=12, $fn=94);
    }
}

Obviously this simple example is not enough to show what MuSCAD allows you to do with ease. Here are a few real-life 3D printer parts designed with MuSCAD:

https://gitlab.com/guillp/muscad/-/raw/master/doc/images/z_stepper_mount.png

A stepper motor attachment. See the OpenSCAD code here and check the MuSCAD code here

https://gitlab.com/guillp/muscad/-/raw/master/doc/images/feet.png

A feet for 3030 extrusion based printers. Check the OpenSCAD code here and the MuSCAD code here

https://gitlab.com/guillp/muscad/-/raw/master/doc/images/z_bed_mount.png

A heating bed attachment for HyperCube-like printers. Here is OpenSCAD code and here is the MuSCAD code

Advantages

The main benefit of using MuSCAD is that you actually write Python code, so you get all the advantages of that language, which include:

  • you can use all Python features, including all syntactic sugar like built-in dict/list comprehensions, external modules such as text parsing, image recognition, etc. which enables very original ways of creating objects.

  • you can use a real IDE, possibly providing code completion, code validity/typos checks, etc. which makes it way easier and faster to write code compared to most generally poor tools used to write OpenSCAD code.

The second advantage, compared to SolidPython and other similar modules, is that MuSCAD lets you write Pythonic, easy to read code, and does not try to emulate the way you would write the same code in OpenSCAD. It enables features that are not part of OpenSCAD, like automatic calculation of object sizes, relative alignment between objects, and more. In that sense, MuSCAD syntax is fundamentally different compared to OpenSCAD/SolidPython. Those differences are explained in the next sections.

Installing MuSCAD

MuSCAD requires Python 3.6 or more and has no other dependency. You obviously need to install OpenSCAD to view the generated files.

Install via PyPI:

pip install muscad

Usage with Jupyter Notebook or Jupyter Lab is recommended.

Using MuSCAD

MuSCAD includes classes for all OpenSCAD primitives. You can instantiate those classes, then apply transformations using methods on those instances, and finally apply boolean operations using the +, - and & operators.

To generate the equivalent OpenSCAD code, simply print() the resulting object.

Example:

from muscad import Sphere
hollow_sphere = Sphere(10) - Sphere(8)
print(hollow_sphere)

will output:

difference() {
  sphere(d=10, $fn=78);
  sphere(d=8, $fn=62);
}

Simply copy/paste that output into OpenSCAD, and it will render the resulting object. To make things easier, you can programmatically save the output code to a file directly, and have that file opened in OpenSCAD (if the openscad executable is in your PATH):

hollow_sphere.render_to_file('hollow_sphere.scad', openscad=True)

If you only want to save the file, without (re)opening OpenSCAD, omit the openscad parameter or set it to False. You may also set the environment variable MUSCAD_NO_OPENSCAD to inhibit MuSCAD from opening OpenSCAD automatically.

Using Jupyter

If you use Jupyter Notebook, it will automatically display the OpenSCAD code for any object in a cell.

How it works

Now, to understand how MuSCAD generates the OpenSCAD code, let’s go over the sample Python code above, line by line:

from muscad import Sphere

This import the primitive Sphere from the top level module muscad. All other primitives can be imported from there.

hollow_sphere = Sphere(10) - Sphere(8)

This generate a MuSCAD object called hollow_sphere, made from the difference of 2 spheres (a smaller one removed from a bigger one).

What is a Sphere ? It is obviously the equivalent of OpenSCAD’s sphere. Like all other primitives, a Sphere is an instance of an Object:

from muscad import Object
assert isinstance(Sphere(10), Object)

Why is that important ? Because there is a lot you can do with a MuSCAD Object, like rendering it, applying transformations to it, using it in boolean operations, aligning it to absolute coordinates, etc. But we’ll see about that later.

Now, what is this hollow_box object? It is a Difference:

from muscad import Difference
assert type(hollow_sphere) == Difference

That Difference is itself a subclass of Object:

from muscad import Object
assert isinstance(hollow_sphere) == Object

So you can do to that difference everything you can do to a primitive. Obviously, you can render the OpenSCAD code, that’s what happens when we call:

print(hollow_sphere)

results in:

difference() {
  sphere(d=10, $fn=78);
  sphere(d=8, $fn=62);
}

Note that when rendering a Sphere, the $fn parameter, which indicates how many segments OpenSCAD must use to render that sphere) is produced automatically by MuSCAD to create a good-looking round shape. In the usual where you want your round shapes to actually appear round, just let MuSCAD handle this for you. Otherwise, you can override that number with the segments parameter to Sphere:

Sphere(20, segments=6)

Primitives

All available primitives from OpenSCAD are available in MuSCAD:

# 3D Primitives
Cube(width, depth, height)  # cube
Cylinder(h, d, d2=None, segments="auto")  # cylinder
Sphere(d, segments="auto")  # sphere
Polyhedron(points, faces, convexity=1)  # polyhedron

# 2D Primitives
Circle(d, segments="auto")  # circle
Square(width, depth)  # square
Text(text, size=10, font=None, halign=None, valign=None, spacing=None, direction=None, language=None, script=None, segments=None)  # text
Polygon(*points, path=None, convexity=None)  # polygon

Note that MuSCAD includes a high level Volume class that is basically a Cube but with a lot of added features, see below.

Boolean Operations

Boolean operations union(), difference() and intersection() are applied using the operators +, - and & respectively:

Sphere(d=5) + Cube(10, 2, 1) + Cube(3, 3, 3).leftward(4)  # union
Sphere(d=5) - Cube(10, 2, 1) - Cube(1, 1, 1)  # difference
Sphere(d=5) & Cube(10, 2, 1) & Cube(1, 4, 2)  # intersection

Beware that standard Python operator precedence applies: + and - applies before &. So the following codes produce different results:

Sphere(d=5) & Cube(10, 2, 1) & Cube(1, 1, 1) + Sphere(50)

and:

(Sphere(d=5) & Cube(10, 2, 1) & Cube(1, 1, 1)) + Sphere(50)

You can also use a more traditional paradigm:

from muscad import Union, Difference, Intersection
union = Union( Sphere(d=5), Cube(10, 2, 1), Cube(3, 3, 3).leftward(4) )
difference = Difference( Sphere(d=5), Cube(10, 2, 1), Cube(1, 1, 1) )
intersection = Intersection( Sphere(d=5), Cube(10, 2, 1), Cube(1, 4, 2) )

As already mentioned above, the result of a boolean operation is itself a MuSCAD Object, so you can keep applying new boolean operations or transformations to it.

Transformations

You can apply transformations to any MuSCAD Object by calling the transformation methods .translate(), .rotate(), etc. Here we translate a Sphere 10 mm upwards:

print(Sphere(10).translate(z=10))

This will give the following OpenSCAD code:

translate(v=[0, 0, 10])
sphere(d=10, $fn=78);

Any Object, including results of boolean operations, or transformed objects, can be applied a transformation. So you can obviously chain multiple methods like this:

Cube(10, 10, 10).translate(15, 15, 0).rotate(0, 45, 0)

MuSCAD includes helpers methods for single axis translations and rotations. Using those helpers, the code just above is equivalent to:

Cube(10, 10, 10).rightward(15).up(15).y_rotate(45)

Note that MuSCAD will automatically merge multiple chained translations or rotations on the same object (this however has no effect on the rendered part). So both the lines above will result in the same generated OpenSCAD code (notice that there is a single translation on the cube combining both translations rightward and upward, instead of several):

rotate(a=[0, 45, 0]) {
    translate(v=[15, 0, 15]) {
        cube(size=[10, 10, 10]);
    }
}

All available transformation methods are as follow:

.translate(x=0, y=0, z=0)  # applies a Translation
.rightward(dist)  # applies a Translation to the right
.leftward(dist)   # applies a Translation to the left
.forward(dist)  # applies a Translation to the front
.backward(dist) # applies a Translation to the back
.up(dist)  # applies a Translation upwards
.down(dist)  # applies a Translation downwards

.rotate(x=0, y=0, z=0)  # applies a Rotation
.x_rotate(angle)  # applies a Rotation on the X axis
.y_rotate(angle)  # applies a Rotation on the Y axis
.z_rotate(angle)  # applies a Rotation on the Z axis
.left_to_bottom()  # turn left face to bottom, alias for .y_rotate(-90)
.left_to_top()  # turn left face to top, alias for .y_rotate(90)
.left_to_front()  # alias for .z_rotate(-90)
.left_to_back()  # alias for .z_rotate(90)
.upside_down(y_axis=False)  # alias for .x_rotate(180) if y_axis==False else .y_rotate(180)

.scale(x=0, y=0, z=0)  # applies a Scaling transformation
.resize(x=0, y=0, z=0)  # applies a Resizing transformation

.mirror(x=0, y=0, z=0)  # applies a Mirroring transformation
.x_mirror(center=0)  # applies a mirroring on X axis, offset by `center`
.y_mirror(center=0)  # applies a mirroring on Y axis, offset by `center`
.z_mirror(center=0)  # applies a mirroring on Z axis, offset by `center`

.linear_extrude(height, center=False, convexity=10, twist=0.0, slices=None, scale=1.0, segments="auto")  # applies a LinearExtrusion
.z_linear_extrude(distance=None, bottom=None, center_z=None, top=None, convexity=10, twist=0.0, slices=None, scale=1.0, segments="auto") # helper to do a LinearExtrusion on the Z axis
.y_linear_extrude(distance=None, back=None, center_y=None, front=None, convexity=10, twist=0.0, slices=None, scale=1.0, segments="auto") # helper to do a LinearExtrusion on the Y axis
.x_linear_extrude(distance=None, left=None, center_x=None, right=None, convexity=10, twist=0.0, slices=None, scale=1.0, segments="auto") # helper to do a LinearExtrusion on the X axis

.rotational_extrude(angle=360, convexity=None, segments="auto")  # applies a RotationalExtrusion

.color(colorname)  # change the object color

.slide(x=0, y=0, z=0)  # applies a Slide transformation

Bounding Box and Alignment

So far we have seen the equivalent of what you can do with OpenSCAD, with a different syntax but without much added value. Here is one of the best added value of MuSCAD: it knows the size and position of any Object, which make it very easy to position or reposition the object at absolute coordinates, or even at a relative position compared to another Object. This is called alignment.

First, let’s understand the dimension of an Object by creating a Cube (which is badly named in OpenSCAD since it can have different dimensions on each axis):

from muscad import Cube
box = Cube(20, 30, 50)

MuSCAD primitives are always created centered, so our box with a width of 20 extends from -10 to +10 on the X axis. MuSCAD knows about that:

assert box.left == -10
assert box.right == 10

Same on the Y and Z axis:

assert box.back == -15
assert box.front == 15
assert box.bottom == -25
assert box.top == 25

The box formed by the leftmost, rightmost, back, front, bottom and top coordinates of an object is called the bounding box.

Since MuSCAD knows the lower and upper bounds on all axis, it can compute the center as well. Since our box was created centered, the center is 0 on all axis:

assert box.center_x == 0
assert box.center_y == 0
assert box.center_z == 0

And obviously it can compute the object width, depth and height (which is easy enough in the case of a Cube, since they are directly declared when the Cube is created):

assert box.width == 20
assert box.depth == 30
assert box.height == 50

MuSCAD is able to calculate the bounding box of all primitives, as well as the results of boolean operations or transformed objects (with some limitations):

assert Cube(10, 20, 30).up(15).bottom == 0
assert (
     Cube(10, 20, 30).leftward(5)
   + Cube(5, 40, 10).up(40)
).height == 60
The limitations are as follow:
  • For rotated objects, MuSCAD is only able to compute the bounding box on an axis rotated by a multiple of 90°.

  • For Differences, the bounding box is that of the first Object (the base object from which all other objects are substracted from), even if a substracted object actually reduces that box.

  • For Intersections, the bounding box is the intersection of the bounding box of all intersected objects. This won’t work accurately from most object shapes, but should be enough for most cases.

Since MuSCAD knows the bounding boxes of all objects it creates, it can also reposition them at absolute coordinates. That is done using .align(). Here we create a box and align its left, back and bottom sides to 0:

aligned_box = Cube(10, 10, 10).align(left=0, back=0, bottom=0)
assert aligned_box.left == aligned_box.back == aligned_box.bottom == 0
assert aligned_box.right == aligned_box.front == aligned_box.top == 10

As you can expect, alignment is done using a translation in the resulting OpenSCAD code:

translate(v=[5.0, 5.0, 5.0])
box(size=[10, 10, 10], center=true);

You can align an Object on the left, right and center_x on the X axis, back, front and center_y on the Y axis, and bottom, top and center_z on the Z axis. Since we can align objects to arbitrary coordinates, and we can get the bounding box coordinates for all objects, we can also align objects relatively to each other. Here we create a tower of 3 colored cubes:

big_cube = Cube(40, 40, 40).color('blue')
medium_cube = (
  Cube(30, 30, 30)
  .color('red')
  .align(center_x=big_cube.center_x, center_y=big_cube.center_y, bottom=big_cube.top)
)
small_cube = (
  Cube(20, 20, 20)
  .color('yellow')
  .align(center_x=medium_cube.center_x, center_y=medium_cube.center_y, bottom=medium_cube.top)
)
print(big_cube + medium_cube + small_cube)

This gives the resulting OpenSCAD code:

union() {
  color("blue")
  cube(size=[40, 40, 40], center=true);
  translate(v=[0.0, 0.0, 35.0])
  color("red")
  cube(size=[30, 30, 30], center=true);
  translate(v=[0.0, 0.0, 60.0])
  color("yellow")
  cube(size=[20, 20, 20], center=true);
}

If you decide later to change the position of the big cube, you only have to change its alignment in the first line of Python code, and the medium and small cube will automatically stay on top of it in the generated OpenSCAD code. That is relative positioning, something that was very hard to do with OpenSCAD, because you had to track the position of objects yourself with variables. MuSCAD does that position tracking for you:

big_cube = Cube(40, 40, 40).color('blue').align(left=10, back=10, bottom=10) # added some alignment for the first cube, the rest of the code is untouched
medium_cube = Cube(30, 30, 30).align(center_x=big_cube.center_x, center_y=big_cube.center_y, bottom=big_cube.top).color('red')
small_cube = Cube(20, 20, 20).align(center_x=medium_cube.center_x, center_y=medium_cube.center_y, bottom=medium_cube.top).color('yellow')
print(big_cube + medium_cube + small_cube)

union() {
  color("blue")
  translate(v=[30.0, 30.0, 30.0])
  box(size=[40, 40, 40], center=true);
  color("red")
  translate(v=[30.0, 30.0, 65.0])
  box(size=[30, 30, 30], center=true);
  color("yellow")
  translate(v=[30.0, 30.0, 90.0])
  box(size=[20, 20, 20], center=true);
}

Epsilon and Tolerance

Often when aligning parts, you want to take into account the tolerance margin for your printer. An appropriate way to do that is to define your tolerance as a constant named T, and add it or substract it whenever needed. MuSCAD includes a default T which is 0.1 mm. If you need a bigger tolerance somewhere, there is a default TT and TTT values that are 0.2 mm and 0.3 mm respectively.

Also, in order to avoid the OpenSCAD “bug” when 2 surfaces are exactly on the same plane, you might want to offset one by a very small value, called an “Epsilon” (something like 0.01 mm). Define this value as a constant named E and add or substract wherever needed. MuSCAD includes a default E value of 0.02 mm, and a “double epsilon” value EE of 0.04 mm.:

from muscad import E, EE, T, TT, TTT

Modifiers

You can apply OpenSCAD modifiers #, %, * and !, by calling the methods .debug(), .background(), .disable(), .root() respectively:

debugged_object = Cube(10, 10, 10).debug()
background_object = Sphere(10).background()
disabled_object = Sphere(20).disable()
root_object = Sphere(5).root()

Volumes

While MuSCAD’s Cube class mimics the OpenSCAD cube primitive, it is too simple to create useful objects. MuSCAD introduces a high-level class called Volume that offers extended possibilities over Cube. For a start, you don’t have to define the size of a Volume, you can specify its lower and upper limits on each axis instead:

from muscad import Volume

my_volume = Volume(left=2, right=10, back=10, front=20, bottom=-4, top=6)
print(my_volume)
translate(v=[6.0, 15.0, 1.0])
cube(size=[8, 10, 10], center=true);

You can also specify the size and one limit on a axis, or the center and the size, or the center and a limit. MuSCAD will extrapolate the rest:

my_volume = Volume(
  left=2, width=8, # this defines the x axis
  center_y=15, depth=10, # Y axis
  center_z=1, top=6) # Z axis

print(my_volume)

This gives the same render as before:

translate(v=[6.0, 15.0, 1.0])
cube(size=[8, 10.0, 10], center=true);

Note that if you specify only one limit or a size for an axis, that axis will be centered on 0 by default.

Fillet & Chamfer

You can fillet or chamfer all edges of a Volume:

chamfered_cube = (
  Volume(width=10, depth=10, height=10)
  .chamfer_all(1) #  apply a chamfer of radius 1 to all edges
)
# warning: this produces more than a hundred lines of OpenSCAD code
print(chamfered_cube)

You can fillet or chamfer specific edges of a Volume:

filleted_side_cube = (
  Volume(width=10, depth=10, height=10)
  .fillet_height(1, left=True) # this fillets the edges along the height of the Volume, restricted to edges on the left side
  .fillet_depth(1, left=True) # this fillets the edges along the depth of the Volume, again restricted to edges on the left side
)
print(filleted_side_cube) # notice that the rendered cube is filleted only on its left side

As the name suggests, using .fillet_width(), .fillet_depth() and .fillet_height() (and the matching chamfer_*() methods) will cut your Volume along its width, depth or height respectively. All 4 fours edges will be cut, unless you select specific edges using the boolean parameters left, right, back, front, bottom and top. By combining 2 of those, you can select the specific edge to cut.

Reverse Fillet & Chamfer

In many situations, instead of cutting the edges of your Volumes, you want to add a fillet to soften an inner edge of a Volume with another part. That’s a reverse fillet or reverse chamfer.

Use the available reverse_fillet_<face>() methods to select the face where reverse fillets will be added, and if needed, select the sides with the boolean parameters left, right, back, front, bottom and top. Here is an example where a reverse fillet will be added at the back of the red part, to join it better with the blue part:

blue_part = Volume(width=40, depth=12, height=6).fillet_height().color("blue")
red_part = (
    Volume(
        width=15,
        back=blue_part.front,
        depth=25,
        center_z=blue_part.center_z,
        height=blue_part.height,
    )
    .reverse_fillet_back(4, left=True, right=True)
    .fillet_height(front=True)
    .color("red")
)

print(blue_part + red_part)

This will create the following object:

https://gitlab.com/guillp/muscad/-/raw/master/doc/images/reverse_fillet.png

Object-Oriented Parts

Now that you are familiar with the basic syntax of primitives, boolean operations, transformations, and alignment, you might want to create complex objects. While this is possible with the basic functional syntax, it will soon create some hard-to-read code, and you might want some ways to better structure you code. MuSCAD offers a Part class that you can inherit to define Objects in an actual Object-Oriented way :)

Here is a simple example, with another tower of cubes, this time with 3 cubes:

from muscad import Part, Cube

class CubeTower(Part):
  def init(self):
    self.add_child(
      Cube(40, 40, 40).color('blue')
    )
    self.add_child(
      Cube(30, 30, 30).align(bottom=self.top).color('red')
    )
    self.add_child(
      Cube(20, 20, 20).align(bottom=self.top).color('yellow')
    )

print(CubeTower())

This gives the same OpenSCAD code as before:

union() {
  color("blue")
  cube(size=[40, 40, 40], center=true);
  color("red")
  translate(v=[0, 0, 35.0])
  cube(size=[30, 30, 30], center=true);
  color("yellow")
  translate(v=[0, 0, 65.0])
  cube(size=[20, 20, 20], center=true);
}

But this Python code doesn’t look like much of an improvement, right? More code to do the same thing? That’s because it doesn’t use the new possibilities offered by the Part class. Before introducing those features, let’s understand what’s going on:

class CubeTower(Part):

This creates a CubeTower class, which inherits MuSCAD Part class. Next:

def init(self):

This special method (not to be confused with Python constructor __init__()) is the Part constructor. It will be executed whenever an object of this class is instantiated. In this constructor:

self.add_child(
  Cube(40, 40, 40).color('blue')
)

Here we create a blue Cube of size 40, and we add it as a child of this Part. The object rendered by a part is made of the sum of its children. Notice that a Part is a MuSCAD object, so MuSCAD can always calculate its bounding box. That’s what we use when we add the second and third Cubes:

self.add_child(
  Cube(30, 30, 30).align(bottom=self.top).color('red')
)
self.add_child(
  Cube(20, 20, 20).align(bottom=self.top).color('yellow')
)

We align that second Cube to the current Part top, which is, before the second Cube is added, the top of the first Cube. Once that second Cube is added, that top “moves up” to include the second Cube, so the third Cube is added on top of it. Since Cubes are created centered by default, the 3 cubes center_x and center_y are aligned to 0.

Finally, once we are done with the CubeTower class definition, we instantiate it and print the resulting OpenSCAD code:

print(CubeTower())

Note that a instantiated Part is a MuSCAD Object, so the bounding box can still be calculated:

tower = CubeTower()
assert tower.top == 70
assert tower.center_x = 0

And the Part can be applied transformations or be used in boolean operations:

assert tower.align(top=0).top == 0
print(tower - Cylinder(d=2, h=tower.height))

Parametric parts

The init() method is executed when you instantiate a Part. You can add parameters to the init() method, which will then be required when instantiating the Part:

class ParametricCubeTower(Part):
  def init(self, biggest_size, narrowing=10, colors=('blue', 'red', 'yellow', 'green')):
    size = biggest_size
    for i, size in enumerate(range(biggest_size, 0, -narrowing)):
      color = colors[i%len(colors)]
      self.add_child(
        Cube(size, size, size)
        .color(color)
        .align(bottom=self.bottom)
      )

Now we start to benefit from Python. The tower of cube will be made from a variable number of cubes depending on the arguments passed at instantiation. So rendering the following code (using the ParametricCube class defined above):

print(ParametricCubeTower(60))

will give:

union() {
  color("blue")
  cube(size=[60, 60, 60], center=true);
  color("red")
  cube(size=[50, 50, 50], center=true);
  color("yellow")
  cube(size=[40, 40, 40], center=true);
  color("green")
  cube(size=[30, 30, 30], center=true);
  color("blue")
  cube(size=[20, 20, 20], center=true);
  color("red")
  cube(size=[10, 10, 10], center=true);
}

But wait! There is more benefits from the Part class.

Non parametrable children

Defining children in the init() method works well for parametrable children, but it is cumbersome when you have to deal with a lot of static, non-parametrable children. Also, because they don’t use any variable parameters, and because you have to call .add_child() everytime, you end with a lot of code noise. To avoid that, you can specify those children as class-level attributes instead. Let’s rewrite our Cube Tower this way:

class StaticCubeTower(Part):
  big_cube = Cube(40, 40, 40).color('blue')
  medium_cube = Cube(30, 30, 30).color('red').align(bottom=big_cube.top)
  small_cube = Cube(20, 20, 20).color('yellow').align(bottom=medium_cube.top)

Since you define class attributes, you have to give each a name. This has 2 additional benefits:

  • you can use previously defined child attributes to align new ones. That’s what we do when we align the medium_cube and small_cube on top of the big_cube and medium_cube respectively. That is more explicit than relying on the implicit current bounding box like we did before with self.

  • the attribute name will be part of the render, as comment, making it easier to find which MuSCAD code produces which OpenSCAD code.

Let’s check those comments:

print(StaticCubeTower())

gives:

union() {
   // big_cube
   color("blue")
   cube(size=[40, 40, 40], center=true);
   // medium_cube
   translate(v=[0, 0, 35.0])
   color("red")
   cube(size=[30, 30, 30], center=true);
   // small_cube
   translate(v=[0, 0, 60.0])
   color("yellow")
   cube(size=[20, 20, 20], center=true);
 }

So you should model Parts by defining first the most constrained children, then designing other child around them.

All class-level attributes that are MuSCAD objects will be automatically added as children once an object of this class is instantiated, and before the init() method is executed. To avoid that, you can prefix the attribute name with an underscore. This will create a non-rendered child.

Non-rendered children

You can add some objects to a Part as a class attribute that will not be rendered, but that you can use them to align with some other children. To do that, simply prefix its name by a _ like this

class MyPart(Part):
  _nonrendered = Cube(10, 10, 10)
  rendered = Sphere(10).align(bottom=_nonrendered.top)

print(MyPart())

Note that any class-level attributes that are not Object, Holes or Misc will not be part of the render. You can use them as class-level variables.

First class (unfillable) holes

Often, when defining an object, its “holes” are more important than its actual filled matter. You might needs those holes for screws, for extrusions, belts, etc. When you create such an hole, you don’t want it to be accidentally filled again when adding more children later. To make things easier, a Part also has a list of holes, that are guaranteed to never be filled by another child. To add such an hole to a Part, simply call add_hole() with the object to remove as parameter. Here is an example, creating 2 crossed pipes:

class CrossedPipes(Part):
  def init(self):
    self.add_child(Cylinder(d=20, h=30))
    self.add_hole(Cylinder(d=15, h=31))
    self.add_child(Cylinder(d=20, h=30).x_rotate(90))
    self.add_hole(Cylinder(d=15, h=31).x_rotate(90))

For this simple non-parametric example, you can also define holes as class-level attributes, by prefixing their definition by the operator ~:

class CrossedPipes(Part):
  vertical_outer_pipe = Cylinder(d=20, h=30)
  vertical_inner_pipe = ~Cylinder(d=15, h=31)
  horizontal_outer_pipe = Cylinder(d=20, h=30).x_rotate(90)
  horizontal_inner_pipe = ~Cylinder(d=15, h=31).x_rotate(90)

Miscellaneous children

Having MuSCAD calculate automatically the dimensions of complex parts is great, but sometimes, there are some Parts that have some miscellaneous features, that you don’t want to be taken into consideration when aligning against other parts. For example, a stepper motor has a shaft protuding on one of its side, but ignoring that shaft makes it easier to align the motor where you want it to be. To create such a misc child, call add_misc(). Here is very simplified Nema17 stepper motor, with the shaft on the top:

class StepperMotor(Part):
  def init(self):
    self.add_child(Cube(42, 42, 50)) # this is the body
    self.add_misc(Cylinder(d=5, h=25).align(bottom=self.top)) # this is the shaft

It is also possible to declare misc children as class-attributes, by calling .misc() on the attribute value:

class StepperMotor(Part):
  body = Cube(42, 42, 50)
  shaft = Cylinder(d=5, h=25).align(bottom=body.top).misc() # this is a misc item

Let’s instantiate it and check its sizes on all axis:

stepper = StepperMotor()
assert stepper.width == 42
assert stepper.depth == 42
assert stepper.height == 50

As you can see, only the body (which is the only “real”, as in “non-misc” child of that Part) determines the size of our simple stepper motor. All misc objects are ignored.

This simple stepper is pretty much useless since it does not include details such as screws. Obviously, you will never want to print a StepperMotor, you will use a real stepper that actually works instead. But you might want a stepper model anyway, that you can use when designing some parts, to align other parts against, or to use as a Hole in another part, such as a stepper attachment. If that stepper model would include all details such as screws, bulge, etc. , the fixation holes would be automatically created in whatever object you align on the fixing holes. Luckily, MuSCAD includes a much more detailed StepperMotor Part in its vitamins (see below). Let’s use it to create a very simple stepper attachment:

from muscad.vitamins.steppers import StepperMotor

class StepperAttachment(Part):
  stepper = ~StepperMotor() # let's create the stepper as a hole
  attachment = Cube(80, stepper.width, 10).align(
    center_x=stepper.center_x, center_y=stepper.center_y, bottom=stepper.top
  ) # that's the simplest attachment you can imagine

Notice that the StepperMotor is a hole since it is prefixed with the operator ~. Note that the order in which normal children and holes are declared does not matter for the rendering, it only matters to allow you to properly align those components together.

Linear Extrusions on all axis

To ease the creation of surfaces that you can extrude, you can use the various methods provided by Surface.

With OpenSCAD, you can only do a linear extrusion on the Z axis (the part “growing” upwards by a given distance). You then have to rotate and align the resulting part manually, which is quite cumbersome. With MuSCAD, you can easily do linear extrusions on all 3 axis by using the methods .x_linear_extrude(), .y_linear_extrude(), and .z_linear_extrude(). They will take care of rotating and aligning your part automatically.

Those methods accept 4 parameters: an extrusion distance, a start (left, back or bottom), and end (right, front or top) and a center (center_x, center_y or center_z). You only have to provide 2 of those, and MuSCAD will extrapolate the rest:

spade = (
    Union(
        Surface.free(
            Circle(d=20).align(right=0, back=5),
            Circle(d=2).align(center_x=0, front=35),
        ),
        Surface.free(
            Circle(d=2).align(right=5, back=0),
            Square(1, 15).align(left=0, back=0),
        ),
    )
    .x_mirror(keep=True)
    .y_linear_extrude(10, center_y=3)
)
https://gitlab.com/guillp/muscad/-/raw/master/doc/images/spade_y.png

Vitamins

MuSCAD includes a library of models for parts commonly used in 3D printing: bolts and nuts, stepper motors, bearings, fans, pulleys, etc. You can have a look at muscad.vitamins.* to use and import them. Most of those models are parametric, so you can have nuts and bolts of any size you like. When there are standardised sizes for some parts, you can instantiate them via class level factories.

Check out the examples to see how they are used.

Example Code

Check out the examples here. Examples include a 3D Printer model that I designed from scratch, inspired by the popular HyperCube Evolution, but with a Dual-Wire XY Gantry. There are a few parts I designed to fix things around the house as well. Feel free to check it out, test it, improve it and share your findings!

Here is a moderately complex example, demonstrating all the features exposed above. This is a feet for 3030 extrusion based printers such as the HyperCube Evolution:

from muscad import Part, Sphere, Volume
from muscad.vitamins.bolts import Bolt
from muscad.vitamins.brackets import CastBracket
from muscad.vitamins.extrusions import Extrusion


class Feet(Part):

    # the next 3 parts are 3030 extrusion models, positionned on each axis, that represents
    # the actual extrusions from the printer
    # the feet parts will be aligned relatively to those extrusions
    z_extrusion = ~Extrusion.e3030(60).background()
    y_extrusion = (
        ~Extrusion.e3030(60)
        .bottom_to_front()
        .align(
            center_x=z_extrusion.center_x,
            back=z_extrusion.front,
            bottom=z_extrusion.bottom,
        )
        .background()
    )
    x_extrusion = (
        ~Extrusion.e3030(60)
        .bottom_to_right()
        .align(
            left=z_extrusion.right,
            center_y=z_extrusion.center_y,
            bottom=z_extrusion.bottom,
        )
        .background()
    )

    # a cast bracket to maintain the X and Y extrusion together
    cast_bracket = (
        ~CastBracket.bracket3030()
        .align(
            left=y_extrusion.right,
            back=x_extrusion.front,
            center_z=x_extrusion.center_z,
        )
        .debug()
    )

    # the base of the feet, that touches the X and Y extrusions
    base = (
        Volume(
            left=z_extrusion.left,
            right=cast_bracket.right,
            back=z_extrusion.back,
            front=cast_bracket.front,
            top=z_extrusion.bottom,
            height=6,
        )
        .fillet_height(10)
        .fillet_height(30, right=True, front=True)
    )

    # bolts to attach the base to the extrusions
    right_bolt = ~Bolt.M6(10).align(
        center_x=base.right - 10,
        center_y=x_extrusion.center_y,
        center_z=x_extrusion.bottom - 2,
    )
    front_bolt = ~Bolt.M6(10).align(
        center_x=y_extrusion.center_x,
        center_y=base.front - 10,
        center_z=y_extrusion.bottom - 2,
    )
    center_bolt = ~Bolt.M8(20).align(
        center_x=z_extrusion.center_x,
        center_y=z_extrusion.center_y,
        center_z=z_extrusion.bottom - 2,
    )

    # let's make the feet height parametrable
    def init(self, height=33):

        self.ball_holder = (
            Volume(
                left=self.z_extrusion.left,
                width=44,
                back=self.z_extrusion.back,
                depth=44,
                top=self.base.bottom,
                height=height,
            )
            .fillet_height(10)
            .fillet_height(20, right=True, front=True)
        )

        # the actual squash ball, making a hole into the feet
        self.squash_ball = (
            ~Sphere(40)
            .align(
                center_x=self.ball_holder.center_x,
                center_y=self.ball_holder.center_y,
                center_z=self.ball_holder.bottom + 1,
            )
            .debug()
        )


if __name__ == "__main__":
    Feet().render_to_file()

Those less than 100 easy-to-read, easy-to-modify lines do render to about 200 hardly-readable, next-to-impossible to modify OpenSCAD lines. Don’t try to modify them! MuSCAD is not designed to create OpenSCAD code that is easy to use for humans. It is intended to use only by OpenSCAD.

The rendered feet was parts of the sample images above.

Contact

Feel free to play with MuSCAD. For any issue, please open a ticket on GitLab. PR are welcome. Guillaume

License

MIT

Some class docstrings are derived from the OpenSCAD User Manual, so are available under the Creative Commons Attribution-ShareAlike License.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

muscad-0.5.1.tar.gz (65.8 kB view hashes)

Uploaded Source

Built Distribution

muscad-0.5.1-py3-none-any.whl (50.1 kB view hashes)

Uploaded Python 3

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page