muscad 0.5.1
OpenSCAD, with a Python Spice
Requires Python: >=3.6,<4.0

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 <https://github.com/SolidCode/SolidPython>`__, which is itself based on
Phillip Tiefenbacher's openscad module, found on
`Thingiverse <http://www.thingiverse.com/thing:1481>`__.

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:

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

   A stepper motor attachment. `See the OpenSCAD code here <https://gitlab.com/guillp/muscad/-/blob/master/tests/target_scad_files/z_stepper_mount.scad>`_ and `check the MuSCAD code here <https://gitlab.com/guillp/muscad/-/blob/master/examples/hdw/z_axis/z_stepper_mount.py>`_

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

   A feet for 3030 extrusion based printers. `Check the OpenSCAD code here <https://gitlab.com/guillp/muscad/-/blob/master/tests/target_scad_files/feet.scad>`_ and `the MuSCAD code here <https://gitlab.com/guillp/muscad/-/blob/master/examples/hdw/misc/feet.py>`_

.. figure:: 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 <https://gitlab.com/guillp/muscad/-/blob/master/tests/target_scad_files/z_bed_mount.scad>`_ and `here is the MuSCAD code <https://gitlab.com/guillp/muscad/-/blob/master/examples/hdw/z_axis/z_bed_mount.py>`_


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 <https://pypi.python.org/pypi/muscad>`__::

       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:

..    figure:: 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)
    )

.. figure:: 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 <https://gitlab.com/guillp/muscad/tree/master/examples>`__.
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
<https://en.wikibooks.org/wiki/OpenSCAD_User_Manual>`__, so
are available under the `Creative Commons Attribution-ShareAlike License
<https://creativecommons.org/licenses/by-sa/3.0/>`__.