piecad.projectbox API documentation
Description: THIS FEATURE IS SOMEWHAT EXPERIMENTAL, THE API SUBJECT TO CHANGE …

# Module `piecad.projectbox`

THIS FEATURE IS SOMEWHAT EXPERIMENTAL, THE API SUBJECT TO CHANGE.

A class for quickly constructing a project box.

Call `finish` to retrieve the Obj3d of the final box.

See `examples` to see how to use ProjectBox (`pbox_*).

The general idea is that ProjectBox creates routines that let you position components in 2D (x, y) on the left, right, front, back, top, bottom of a ProjectBox. Generally 2D design is easier than 3D.

Components are shapes or holes. Holes names end in "_hole" or "holes". When a ProjectBox is finished, all shapes are unioned with the box and all holes are differenced from the box.

By convention component origins (0, 0) are either centered, or bottom left. It is a good idea to show the ORIGIN in the components documentation.

## Classes

`class ProjectBox (size: list[float, float, float], wall: float = 2.0, segments: int = -1)`
  Expand source code

      class ProjectBox:
          """
          Quickly create a project box.
          """
          def __init__(self,
              size: list[float, float, float],
              wall: float = 2.0,
              segments: int = -1,
          ):
              """
              Make a project box with the x (width), y (depth), and z (height) values given in size.

              The dimensions are for the INSIDE of the box.

              The INSIDE of the box will be placed at `(0, 0, 0)`.

              The `wall` parameter specifies the thickness of the major walls of the box.

              For `segments` see the documentation of [`set_default_segments`](index.html#piecad.set_default_segments).

              <iframe width="100%" height="380" src="/examples/projectbox.html"></iframe>
              """
              if segments == -1:
                  segments = config["DefaultSegments"]
              _chkGE("segments", segments, 3)
              _chkV3("size", size)
              _chkGE("wall", wall, 2.0)

              tiny = 0.5
              self._segments = segments
              self._size = size
              self.w, self.d, self.h = self._size
              self.h = self.h+wall
              self.wall = wall
              self.off = 2*wall+tiny

              def mkwall(dims, ty=None):
                  screw_h = 10
                  screw_top_r = 3.0
                  screw_r = (screw_top_r/2)*0.8 # 3mm screw hole
                  rd = (screw_top_r+2*wall)+tiny
                  rr = rd/2.0
                  off = self.off

                  def post(rot):
                      post_l = 2*rr
                      post_w = 3*wall+tiny
                      return difference(
                          union(
                              cylinder(radius=rr, height=self.h),
                              cube([post_l, post_w, self.h]).translate([0, -rr, 0]),
                              cube([post_w, post_l, self.h]).translate([-rr, 0, 0]),
                          ),
                          cylinder(radius=screw_r, height=screw_h).translate([0, 0, self.h-screw_h]),
                      ).translate([rr, rr, 0]).rotate([0, 0, rot])

                  if ty == 't' or ty == 'm':
                      w = dims[0]+2*off
                      d = dims[1]+2*off
                      h = dims[2]
                      o = rounded_rectangle([w, d], rr, segments).extrude(h)
                      if ty == 't':
                          o = union(o,
                             post(0),
                             post(270).translate([0, d, 0]),
                             post(180).translate([w, d, 0]),
                             post(90).translate([w, 0, 0]),
                          )
                          o = difference(
                              o,
                              cube([w-4*rr, wall+tiny, self.h]).translate([2.0*rr,wall,wall]),
                              cube([w-4*rr, wall+tiny, self.h]).translate([2.0*rr,d-2*wall,wall]),
                              cube([wall+tiny, d-4*rr, self.h]).translate([wall, 2.0*rr, wall]),
                              cube([wall+tiny, d-4*rr, self.h]).translate([w-2*wall,2.0*rr,wall]),
                          )
                      else:
                          def tbh():
                              chamfer_h = sin(45)*screw_top_r
                              h_chamfered = screw_h if chamfer_h>screw_h else chamfer_h
                              h_remaining = 0 if chamfer_h>screw_h else screw_h-chamfer_h
                              o = union(
                                  cylinder(height=0.5, radius=screw_top_r),
                                  cone(height=h_chamfered, radius_low=screw_top_r, radius_high=screw_r).translate((0, 0, 0.5)),
                                  cylinder(height=h_remaining, radius=screw_r).translate((0, 0, h_chamfered+1)),
                              )
                              return o

                          o = difference(
                              o,
                              tbh().translate([rr, rr, 0]),
                              tbh().translate([w-rr, rr, 0]),
                              tbh().translate([rr, d-rr, 0]),
                              tbh().translate([w-rr, d-rr, 0]),
                          )
                      o = o.translate([-off, -off, -wall])
                  else:
                      w = dims[0]-2*rr-tiny+wall
                      d = dims[1]-wall
                      h = dims[2]
                      o = cube([w, d, h])
                      o = o.translate([0, 0, -wall])
                  return o

              self._l_wall = mkwall([self.d, self.h, self.wall])
              self._l_unions = []
              self._l_differences = []
              self._r_wall = mkwall([self.d, self.h, self.wall])
              self._r_unions = []
              self._r_differences = []
              self._f_wall = mkwall([self.w, self.h, self.wall])
              self._f_unions = []
              self._f_differences = []
              self._k_wall = mkwall([self.w, self.h, self.wall])
              self._k_unions = []
              self._k_differences = []
              self._t_wall = mkwall([self.w, self.d, self.wall], 't')
              self._t_unions = []
              self._t_differences = []
              self._m_wall = mkwall([self.w, self.d, self.wall], 'm')
              self._m_unions = []
              self._m_differences = []

          def left(self, x, y, shape, hole):
              """
              Add a component at `x, y` of the left side of the box.
              If `shape` or `hole` is not present, use `None`.
              """
              if shape != None:
                  self._l_unions.append(shape.translate([x, y, 0]))
              if hole != None:
                  self._l_differences.append(hole.translate([x, y, 0]))

          def right(self, x, y, shape, hole):
              """
              Add a component at `x, y` of the right side of the box.
              If `shape` or `hole` is not present, use `None`.
              """
              if shape != None:
                  self._r_unions.append(shape.translate([x, y, 0]))
              if hole != None:
                  self._r_differences.append(hole.translate([x, y, 0]))

          def front(self, x, y, shape, hole):
              """
              Add a component at `x, y` of the front side of the box.
              If `shape` or `hole` is not present, use `None`.
              """
              if shape != None:
                  self._f_unions.append(shape.translate([x, y, 0]))
              if hole != None:
                  self._f_differences.append(hole.translate([x, y, 0]))

          def back(self, x, y, shape, hole):
              """
              Add a component at `x, y` of the back side of the box.
              If `shape` or `hole` is not present, use `None`.
              """
              if shape != None:
                  self._k_unions.append(shape.translate([x, y, 0]))
              if hole != None:
                  self._k_differences.append(hole.translate([x, y, 0]))

          def top(self, x, y, shape, hole):
              """
              Add a component at `x, y` of the top of the box.
              If `shape` or `hole` is not present, use `None`.
              """
              if shape != None:
                  self._t_unions.append(shape.translate([x, y, 0]))
              if hole != None:
                  self._t_differences.append(hole.translate([x, y, 0]))

          def bottom(self, x, y, shape, hole):
              """
              Add a component at `x, y` of the bottom of the box.
              If `shape` or `hole` is not present, use `None`.
              """
              if shape != None:
                  self._m_unions.append(shape.translate([x, y, 0]))
              if hole != None:
                  self._m_differences.append(hole.translate([x, y, 0]))

          def finish(self) -> Obj3d:
              """
              The `finish` method returns a six-tuple of the parts of the project box:

              ```
              left, right, front, back, top, bottom = ProjectBox((45, 30, 20)).finish()
              ```

              """
              def f(box, differences, unions):
                  if len(unions) > 0:
                      box = union(box, *unions)
                  if len(differences) > 0:
                      box = difference(box, *differences)
                  return box
              return (
                      f(self._l_wall, self._l_differences, self._l_unions),
                      f(self._r_wall, self._r_differences, self._r_unions),
                      f(self._f_wall, self._f_differences, self._f_unions),
                      f(self._k_wall, self._k_differences, self._k_unions),
                      f(self._t_wall, self._t_differences, self._t_unions),
                      f(self._m_wall, self._m_differences, self._m_unions),
                      )

  Quickly create a project box.

  Make a project box with the x (width), y (depth), and z (height) values given in size.

  The dimensions are for the INSIDE of the box.

  The INSIDE of the box will be placed at `(0, 0, 0)`.

  The `wall` parameter specifies the thickness of the major walls of the box.

  For `segments` see the documentation of `set_default_segments`.

  ### Methods

  `def back(self, x, y, shape, hole)`
    Expand source code

        def back(self, x, y, shape, hole):
            """
            Add a component at `x, y` of the back side of the box.
            If `shape` or `hole` is not present, use `None`.
            """
            if shape != None:
                self._k_unions.append(shape.translate([x, y, 0]))
            if hole != None:
                self._k_differences.append(hole.translate([x, y, 0]))

    Add a component at `x, y` of the back side of the box. If `shape` or `hole` is not present, use `None`.

  `def bottom(self, x, y, shape, hole)`
    Expand source code

        def bottom(self, x, y, shape, hole):
            """
            Add a component at `x, y` of the bottom of the box.
            If `shape` or `hole` is not present, use `None`.
            """
            if shape != None:
                self._m_unions.append(shape.translate([x, y, 0]))
            if hole != None:
                self._m_differences.append(hole.translate([x, y, 0]))

    Add a component at `x, y` of the bottom of the box. If `shape` or `hole` is not present, use `None`.

  `def finish(self) ‑> Obj3d`
    Expand source code

        def finish(self) -> Obj3d:
            """
            The `finish` method returns a six-tuple of the parts of the project box:

            ```
            left, right, front, back, top, bottom = ProjectBox((45, 30, 20)).finish()
            ```

            """
            def f(box, differences, unions):
                if len(unions) > 0:
                    box = union(box, *unions)
                if len(differences) > 0:
                    box = difference(box, *differences)
                return box
            return (
                    f(self._l_wall, self._l_differences, self._l_unions),
                    f(self._r_wall, self._r_differences, self._r_unions),
                    f(self._f_wall, self._f_differences, self._f_unions),
                    f(self._k_wall, self._k_differences, self._k_unions),
                    f(self._t_wall, self._t_differences, self._t_unions),
                    f(self._m_wall, self._m_differences, self._m_unions),
                    )

    The `finish` method returns a six-tuple of the parts of the project box:

        left, right, front, back, top, bottom = ProjectBox((45, 30, 20)).finish()

  `def front(self, x, y, shape, hole)`
    Expand source code

        def front(self, x, y, shape, hole):
            """
            Add a component at `x, y` of the front side of the box.
            If `shape` or `hole` is not present, use `None`.
            """
            if shape != None:
                self._f_unions.append(shape.translate([x, y, 0]))
            if hole != None:
                self._f_differences.append(hole.translate([x, y, 0]))

    Add a component at `x, y` of the front side of the box. If `shape` or `hole` is not present, use `None`.

  `def left(self, x, y, shape, hole)`
    Expand source code

        def left(self, x, y, shape, hole):
            """
            Add a component at `x, y` of the left side of the box.
            If `shape` or `hole` is not present, use `None`.
            """
            if shape != None:
                self._l_unions.append(shape.translate([x, y, 0]))
            if hole != None:
                self._l_differences.append(hole.translate([x, y, 0]))

    Add a component at `x, y` of the left side of the box. If `shape` or `hole` is not present, use `None`.

  `def right(self, x, y, shape, hole)`
    Expand source code

        def right(self, x, y, shape, hole):
            """
            Add a component at `x, y` of the right side of the box.
            If `shape` or `hole` is not present, use `None`.
            """
            if shape != None:
                self._r_unions.append(shape.translate([x, y, 0]))
            if hole != None:
                self._r_differences.append(hole.translate([x, y, 0]))

    Add a component at `x, y` of the right side of the box. If `shape` or `hole` is not present, use `None`.

  `def top(self, x, y, shape, hole)`
    Expand source code

        def top(self, x, y, shape, hole):
            """
            Add a component at `x, y` of the top of the box.
            If `shape` or `hole` is not present, use `None`.
            """
            if shape != None:
                self._t_unions.append(shape.translate([x, y, 0]))
            if hole != None:
                self._t_differences.append(hole.translate([x, y, 0]))

    Add a component at `x, y` of the top of the box. If `shape` or `hole` is not present, use `None`.

`class pbc`
  Expand source code

      class pbc:
          @staticmethod
          def tap_post(h: float, size_d: float, post_wall=2.0, add_taper=False, z_rot=0):
              """
              A post suitable for tapping or using a self-taping screw or bolt.
              ORIGIN: centered

              Parameter `h` is the height of the post (without taper).

              Parameter `size_d` is a diameter because that's how bolts are usually
              specified. For example a M4 bolt has a 4mm diameter.

              Parameter `post_wall`, how thick (beyond the screw) the wall should be.

              If you are mounting your posts horizontally (from the sides of the box),
              set `add_taper` to `True` and a 45 degree taper is added to the bottom
              of the post to aid in 3d printing.

              By default a tap post's bottom is centered at (0, 0, 0), BUT, if you
              add a taper then the tap post's top is centered at (0, 0, 0).

              Parameter `z_rot` is if you want to rotate the post (used in some lids).
              """
              inner_d = size_d*0.8 #Possibly snug, but with PLA I prefer that
              outer_d = post_wall*2+size_d
              circ = difference(circle(outer_d/2.0), circle(inner_d/2.0))
              if (add_taper):
                  tp = circ.extrude(h*2)
                  tp = difference(tp,
                      cube([outer_d, outer_d, h*3+2], center=True).rotate([45, 0, z_rot]).translate([0,0,h]))
                  return tp.translate([0, 0, -h*2])
              else:
                  return circ.extrude(h)

          @staticmethod
          def horizontal_slot_hole(w, h, wall=2):
              """
              Creates a hole that is a rounded_rectangle.
              Useful for things like a USB connector.

              ORIGIN: centered
              """
              return rounded_rectangle((w, h), h/2.0).extrude(wall).rotate((180, 0, 0)).translate((-(w+h)/2.0, 0, 0))

          @staticmethod
          def tapered_bolt_hole(height, size_d):
              """
              Creates a hole for a tapered bolt (the top of the head will be flush).

              ORIGIN: centered
              """
              bolt_top_r = size_d
              r = size_d/2.0
              chamfer_h = sin(45)*bolt_top_r
              h_chamfered = height if chamfer_h>height else chamfer_h
              h_remaining = 0 if chamfer_h>height else height-chamfer_h
              o = union(
                  cylinder(height=1, radius=bolt_top_r),
                  cone(height=h_chamfered, radius_low=bolt_top_r, radius_high=r).translate((0, 0, 1)),
                  cylinder(height=h_remaining, radius=r*0.80).translate((0, 0, h_chamfered+1)),
              ).rotate((180, 0, 0))
              return o

          @staticmethod
          def hole(r, wall=2):
              """
              Creates a negative shape to create a hole.
              (Used in difference, or the "hole" attribute in ProjectBox methods.)

              ORIGIN: centered
              """
              return circle(r).extrude(wall).rotate((180, 0, 0))

          @staticmethod
          def wire_tie_loop():
              """
              A "horseshoe" sized to pass a wire tie through.
              Useful for holding project boards in place with no screws.

              ORIGIN: centered
              """
              tiny = 0.01
              return difference(
                  cube([2, 7, 6], center=True),
                  cube([2+2*tiny, 3, 2], center=True),
              ).translate([1,3.5,3])

          @staticmethod
          def circular_speaker_grid_holes(radius, wall=2, hole_w=2):
              """
              The `radius` is the radius of the speaker grill.
              Also useful for air holes.

              Parameter `wall` specifies the width of the grid holes.
              Parameter `hole_w` specifies the width of the grid holes.

              Works best if speaker `radius` is evenly divisible by `hole_w`.

              ORIGIN: centered
              """
              tiny = 0.01
              l = []
              y = -(radius - hole_w)
              while y <= radius - hole_w:
                  l.append(
                      cube([radius * 2, hole_w, wall + 2 * tiny], center=True).translate([0, y, 0])
                  )
                  y += 2 * hole_w
              return intersect(
                  union(*l), cylinder(radius=radius, height=wall + 2 * tiny, center=True)
              ).translate([0, 0, -(wall / 2) - tiny])

  ### Static methods

  `def circular_speaker_grid_holes(radius, wall=2, hole_w=2)`
    Expand source code

        @staticmethod
        def circular_speaker_grid_holes(radius, wall=2, hole_w=2):
            """
            The `radius` is the radius of the speaker grill.
            Also useful for air holes.

            Parameter `wall` specifies the width of the grid holes.
            Parameter `hole_w` specifies the width of the grid holes.

            Works best if speaker `radius` is evenly divisible by `hole_w`.

            ORIGIN: centered
            """
            tiny = 0.01
            l = []
            y = -(radius - hole_w)
            while y <= radius - hole_w:
                l.append(
                    cube([radius * 2, hole_w, wall + 2 * tiny], center=True).translate([0, y, 0])
                )
                y += 2 * hole_w
            return intersect(
                union(*l), cylinder(radius=radius, height=wall + 2 * tiny, center=True)
            ).translate([0, 0, -(wall / 2) - tiny])

    The `radius` is the radius of the speaker grill. Also useful for air holes.

    Parameter `wall` specifies the width of the grid holes. Parameter `hole_w` specifies the width of the grid holes.

    Works best if speaker `radius` is evenly divisible by `hole_w`.

    ORIGIN: centered

  `def hole(r, wall=2)`
    Expand source code

        @staticmethod
        def hole(r, wall=2):
            """
            Creates a negative shape to create a hole.
            (Used in difference, or the "hole" attribute in ProjectBox methods.)

            ORIGIN: centered
            """
            return circle(r).extrude(wall).rotate((180, 0, 0))

    Creates a negative shape to create a hole. (Used in difference, or the "hole" attribute in ProjectBox methods.)

    ORIGIN: centered

  `def horizontal_slot_hole(w, h, wall=2)`
    Expand source code

        @staticmethod
        def horizontal_slot_hole(w, h, wall=2):
            """
            Creates a hole that is a rounded_rectangle.
            Useful for things like a USB connector.

            ORIGIN: centered
            """
            return rounded_rectangle((w, h), h/2.0).extrude(wall).rotate((180, 0, 0)).translate((-(w+h)/2.0, 0, 0))

    Creates a hole that is a rounded_rectangle. Useful for things like a USB connector.

    ORIGIN: centered

  `def tap_post(h: float, size_d: float, post_wall=2.0, add_taper=False, z_rot=0)`
    Expand source code

        @staticmethod
        def tap_post(h: float, size_d: float, post_wall=2.0, add_taper=False, z_rot=0):
            """
            A post suitable for tapping or using a self-taping screw or bolt.
            ORIGIN: centered

            Parameter `h` is the height of the post (without taper).

            Parameter `size_d` is a diameter because that's how bolts are usually
            specified. For example a M4 bolt has a 4mm diameter.

            Parameter `post_wall`, how thick (beyond the screw) the wall should be.

            If you are mounting your posts horizontally (from the sides of the box),
            set `add_taper` to `True` and a 45 degree taper is added to the bottom
            of the post to aid in 3d printing.

            By default a tap post's bottom is centered at (0, 0, 0), BUT, if you
            add a taper then the tap post's top is centered at (0, 0, 0).

            Parameter `z_rot` is if you want to rotate the post (used in some lids).
            """
            inner_d = size_d*0.8 #Possibly snug, but with PLA I prefer that
            outer_d = post_wall*2+size_d
            circ = difference(circle(outer_d/2.0), circle(inner_d/2.0))
            if (add_taper):
                tp = circ.extrude(h*2)
                tp = difference(tp,
                    cube([outer_d, outer_d, h*3+2], center=True).rotate([45, 0, z_rot]).translate([0,0,h]))
                return tp.translate([0, 0, -h*2])
            else:
                return circ.extrude(h)

    A post suitable for tapping or using a self-taping screw or bolt. ORIGIN: centered

    Parameter `h` is the height of the post (without taper).

    Parameter `size_d` is a diameter because that's how bolts are usually specified. For example a M4 bolt has a 4mm diameter.

    Parameter `post_wall`, how thick (beyond the screw) the wall should be.

    If you are mounting your posts horizontally (from the sides of the box), set `add_taper` to `True` and a 45 degree taper is added to the bottom of the post to aid in 3d printing.

    By default a tap post's bottom is centered at (0, 0, 0), BUT, if you add a taper then the tap post's top is centered at (0, 0, 0).

    Parameter `z_rot` is if you want to rotate the post (used in some lids).

  `def tapered_bolt_hole(height, size_d)`
    Expand source code

        @staticmethod
        def tapered_bolt_hole(height, size_d):
            """
            Creates a hole for a tapered bolt (the top of the head will be flush).

            ORIGIN: centered
            """
            bolt_top_r = size_d
            r = size_d/2.0
            chamfer_h = sin(45)*bolt_top_r
            h_chamfered = height if chamfer_h>height else chamfer_h
            h_remaining = 0 if chamfer_h>height else height-chamfer_h
            o = union(
                cylinder(height=1, radius=bolt_top_r),
                cone(height=h_chamfered, radius_low=bolt_top_r, radius_high=r).translate((0, 0, 1)),
                cylinder(height=h_remaining, radius=r*0.80).translate((0, 0, h_chamfered+1)),
            ).rotate((180, 0, 0))
            return o

    Creates a hole for a tapered bolt (the top of the head will be flush).

    ORIGIN: centered

  `def wire_tie_loop()`
    Expand source code

        @staticmethod
        def wire_tie_loop():
            """
            A "horseshoe" sized to pass a wire tie through.
            Useful for holding project boards in place with no screws.

            ORIGIN: centered
            """
            tiny = 0.01
            return difference(
                cube([2, 7, 6], center=True),
                cube([2+2*tiny, 3, 2], center=True),
            ).translate([1,3.5,3])

    A "horseshoe" sized to pass a wire tie through. Useful for holding project boards in place with no screws.

    ORIGIN: centered