Package piecad

"Easy as Pie" CAD (Piecad)

It is my opinionated view of what a good, simple CAD API should look like.

For many years I used OpenSCAD, but the functional language it uses was often a hindrance, and its speed was poor.

Piecad is based on Manifold. Manifold incorporates Clipper2 for 2D objects. It also uses quickhull for 3d convex hulls. You can see Manifold's web site for other packages that are used.

See the file for more packages that we use.

Piecad Version

Sub-modules

piecad.bulk_ops

Miscellaneous bulk operations that work on multiple objects.

piecad.lin_math

This is a small package of linear algebra functions for 2D and 3D vectors. It is used internally in Piecad because Numpy is actually rather slow for …

piecad.primitives_2d

Create 2D objects such as circles and retangles.

piecad.primitives_3d

Create 3D objects such as spheres and cubes.

piecad.projectbox

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

piecad.trigonometry

Trigonometry functions that use and return degrees …

piecad.utilities

Miscellaneous (but important) functions

Global variables

var Const

Const a namedtuple holding global constants for the Piecad library.

Const.epsilon - The default epsilon value for precision comparisons.
Const.sqrt_2 - The square root of 2.

Functions

def near_eq(a, b, epsilon=1e-12) ‑> bool
Expand source code
def near_eq(a, b, epsilon=Const.epsilon) -> bool:
    """
    If `a` is near `b` (within epsilon) then return True` else `return False`
    """
    return abs(a - b) <= epsilon

If a is near b (within epsilon) then return True else return False`

def near_ge(a, b, epsilon=1e-12) ‑> bool
Expand source code
def near_ge(a, b, epsilon=Const.epsilon) -> bool:
    """
    If `a - epsilon` is greater than or equal to `b` then `return True` else `return False`
    """
    return a - epsilon >= b

If a - epsilon is greater than or equal to b then return True else return False

def near_le(a, b, epsilon=1e-12) ‑> bool
Expand source code
def near_le(a, b, epsilon=Const.epsilon) -> bool:
    """
    If `a + epsilon` is less than or equal to `b` then `return True` else `return False`
    """
    return a + epsilon <= b

If a + epsilon is less than or equal to b then return True else return False

def version() ‑> str
Expand source code
def version() -> str:
    "Piecad version"
    return __version__

Piecad version

Classes

class Config
Expand source code
class Config:
    """
    User changeable global settings.

    You can use these in your script and you can also create a file called `.piecadrc`
    in your home directory to set these values every time you run a Piecad script.
    Your `.piecadrc` can only contain `Config` calls and `print` statements.

    Example `.piecadrc`:

    ```
    Config.set_default_color("tan")
    print("Changed default color:", Config.get_default_color())
    ```
    """

    def __init__(self):
        pass

    _default_segments = 36
    _default_units = "mm"
    _layer_resolution = 0.1
    _default_color = _parse_color("tan")

    # Prevent instantiation
    # def __new__(cls, *args, **kwargs):
    # raise TypeError(f"{cls.__name__} cannot be instantiated.")

    @classmethod
    def get_default_segments(cls) -> int:
        """
        Get the default number of segments used by circular objects.
        """
        return cls._default_segments

    @classmethod
    def set_default_segments(cls, segments: int = 36) -> None:
        """
        Set the default number of segments used by circular objects.

        Functions that produce circular objects need to know how
        many segments should be used to draw the "circle".
        For example if you call the `circle` function with `segments = 4`
        it will produce a square... in most cases undesirable, though
        a `circle` with `segments = 6` will produce a hexagon which
        can be useful.
        In most cases a higher number is desired as it produces objects
        that look truly round.

        The default value of `segments` 36.

        If you need a circular objects primary axes to have exact values (at the
        90 degree marks), choose a `segments` value that is a multiple of 4.

        In circular functions, if the value passed in for `segments` is `-1`, then
        the `default_segments` value is used. Thus circular functions have
        a default value for `segments` of `-1`.
        """
        _chkGE("segments", segments, 3)
        cls._default_segments = segments

    @classmethod
    def get_default_units(cls) -> str:
        """
        Get the default units used by this script.
        This defaults to "mm" for millimeters.
        It may also be "cm" for centimeters or "in" for inches.
        """
        return cls._default_units

    @classmethod
    def set_default_units(cls, units: str = "mm") -> None:
        """
        Set the default units used in this script.
        Most 3D printing uses "mm" for millimeters.
        But it can also be "cm" for centimeters or "in" for inches.
        """
        _chkIn("units", units, ["mm", "cm", "in"])
        cls._default_units = units

    @classmethod
    def get_layer_resolution(cls) -> float:
        """
        Get the layer resolution used in printing in this script.
        If you have more than one resolution, use the smallest.
        For most 3D printing, the default of 0.1 is sufficient.
        """
        return cls._layer_resolution

    @classmethod
    def set_layer_resolution(cls, resolution: float) -> None:
        """
        Set the layer resolution used in printing in this script.
        If you have more than one resolution, use the smallest.
        For most 3D printing, the default of 0.1 is sufficient.
        """
        _chkGT("resolution", resolution, 0)
        cls._layer_resolution = resolution

    @classmethod
    def get_default_color(cls) -> tuple[int, int, int]:
        """
        Returns the RGB values for the current default color.
        """
        return cls._default_color

    @classmethod
    def set_default_color(cls, cspec: tuple[int, int, int] | str) -> None:
        """
        Assign the given color for use as a default color for objects.</summary>
            The `cspec` parameter has 3 formats:

            *   A tuple of RGB color values, where each value is between
                0 and 255. Such as: `(255, 128, 0)`

            * A string beginning with "#" followed by 6 hex digits
                representing RGB. Such as "#FF00FF"
            * A string that is one of the basic or extended CSS color names.
              For a list of color names see: [Color keywords](https://www.w3.org/wiki/CSS/Properties/color    /keywords)
        """
        cls._default_color = _parse_color(cspec)

User changeable global settings.

You can use these in your script and you can also create a file called .piecadrc in your home directory to set these values every time you run a Piecad script. Your .piecadrc can only contain Config calls and print statements.

Example .piecadrc:

Config.set_default_color("tan")
print("Changed default color:", Config.get_default_color())

Static methods

def get_default_color() ‑> tuple[int, int, int]

Returns the RGB values for the current default color.

def get_default_segments() ‑> int

Get the default number of segments used by circular objects.

def get_default_units() ‑> str

Get the default units used by this script. This defaults to "mm" for millimeters. It may also be "cm" for centimeters or "in" for inches.

def get_layer_resolution() ‑> float

Get the layer resolution used in printing in this script. If you have more than one resolution, use the smallest. For most 3D printing, the default of 0.1 is sufficient.

def set_default_color(cspec: tuple[int, int, int] | str) ‑> None

Assign the given color for use as a default color for objects. The cspec parameter has 3 formats:

*   A tuple of RGB color values, where each value is between
    0 and 255. Such as: <code>(255, 128, 0)</code>

* A string beginning with "#" followed by 6 hex digits
    representing RGB. Such as "#FF00FF"
* A string that is one of the basic or extended CSS color names.
  For a list of color names see: [Color keywords](https://www.w3.org/wiki/CSS/Properties/color    /keywords)
def set_default_segments(segments: int = 36) ‑> None

Set the default number of segments used by circular objects.

Functions that produce circular objects need to know how many segments should be used to draw the "circle". For example if you call the circle function with segments = 4 it will produce a square… in most cases undesirable, though a circle with segments = 6 will produce a hexagon which can be useful. In most cases a higher number is desired as it produces objects that look truly round.

The default value of segments 36.

If you need a circular objects primary axes to have exact values (at the 90 degree marks), choose a segments value that is a multiple of 4.

In circular functions, if the value passed in for segments is -1, then the default_segments value is used. Thus circular functions have a default value for segments of -1.

def set_default_units(units: str = 'mm') ‑> None

Set the default units used in this script. Most 3D printing uses "mm" for millimeters. But it can also be "cm" for centimeters or "in" for inches.

def set_layer_resolution(resolution: float) ‑> None

Set the layer resolution used in printing in this script. If you have more than one resolution, use the smallest. For most 3D printing, the default of 0.1 is sufficient.

class Obj2d (o: object = None, color=None)
Expand source code
class Obj2d:
    """
    Wrapper class for "CrossSections", which are 2D graphical objects.

    Attributes:
        mo The Manifold::CrossSection object used by manifold3d.
    """

    def __init__(self, o: object = None, color=None) -> None:
        if o == None:
            o = _m.CrossSection()
        self.mo = o
        self._color = color

    def area(self) -> float:
        """
        The area of this Obj2d.
        """
        return self.mo.area()

    def bounding_box(self) -> tuple[float, float, float, float]:
        """
        Return the bounding box of this object.

        Return a tuple: (left, bottom, right, top) which represents
        a rectangle that would exactly contain this object.

        It can be broken up like this: `x1, y1, x2, y2 = obj.bounding_box()`
        """
        return self.mo.bounds()

    def center(
        self, axes: tuple[bool, bool] = (True, True), at: tuple[float, float] = (0, 0)
    ) -> Obj2d:
        """
        Center the object on each of the `True` axes.

        Parameter `at` specifies the point to center on.

        """
        xmin, ymin, xmax, ymax = self.bounding_box()
        mid_x = (xmax - xmin) / 2 + xmin
        mid_y = (ymax - ymin) / 2 + ymin
        new_x = 0
        new_y = 0
        if axes[0]:
            new_x = at[0] - mid_x
        if axes[1]:
            new_y = at[1] - mid_y
        o2 = self.translate((new_x, new_y))
        o2._color = self._color
        return o2

    def color(self, cspec: tuple[int, int, int] | str) -> Obj2d:
        """
        Assign the given color to this object.</summary>

        Parameters:

            The `cspec` parameter has 3 formats:

            *   A tuple of RGB color values, where each value is between
                0 and 255. Such as: `(255, 128, 0)`

            * A string beginning with "#" followed by 6 hex digits
                representing RGB. Such as "#FF00FF"
            * A string that is one of the basic or extended CSS color names.
              For a list of color names see: [Color keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords)
        """
        return Obj2d(self.mo, _parse_color(cspec))

    def corner(self, at: tuple[float, float] = (0, 0)) -> Obj2d:
        """
        Move the bounding box minimum corner to (0, 0).

        Parameter `at` specifies an alternate point to move the corner to.
        """
        xmin, ymin, xmax, ymax = self.bounding_box()
        new_x = at[0] - xmin
        new_y = at[1] - ymin
        o2 = self.translate((new_x, new_y))
        o2._color = self._color
        return o2

    def decompose(self) -> list[Obj2d]:
        """
        Decompose this object into a list of topologically disjoint objects.

        """
        ml = self.mo.decompose()
        l = []
        for m in ml:
            l.append(Obj2d(m, color=self._color))
        return l

    def is_empty(self) -> bool:
        """
        Is this object empty?
        """
        return self.mo.is_empty()

    def extrude(self, height: int) -> Obj3d:
        """
        Extrude this object into a Obj3d of the given height.
        """
        o3 = Obj3d(_m.Manifold.extrude(self.mo, height))
        return o3

    def mirror(self, axes: tuple[bool, bool]) -> Obj2d:
        """
        Mirror this object around the given axes.

        To understand this, pretend you are holding you right hand up in front of a mirror.
        Consider the hand in mirror the original image, but flattened to 2D.
        Mirror over the x axis and you will get an image to the left that looks like a left hand.
        Mirror over the y axis and you will get an image of the right hand upside down.
        Mirror over the x and y axes and you will get an image a left hand upside down.

        From a mathematical standpoint, mirroring negates all points on each selected axis.

        """
        uv = [0, 0]
        if axes[0]:
            uv[0] = 1
        if axes[1]:
            uv[1] = 1
        return Obj2d(self.mo.mirror(uv), self._color)

    def num_verts(self) -> int:
        """
        The number of vertices in this object.

        This is useful in unit testing shapes.

        It is very difficult to check that a shape is correct, but knowing that a correct
        number of vertices is present goes a long way to making sure things are healthy.
        """
        return self.mo.num_vert()

    def offset(
        self, delta: float, join_type: str, miter_limit: float = 2.0, segments: int = -1
    ) -> Obj2d:
        """
        Offset (or inset) a 2D object by a given distance called `delta`.
        A negative delta will cause an inset to be done.

        Select `join_type` from `"square"`, `"round"`, or `"miter"`.
        To understand `miter_limit`, see: [Clipper2 MiterLimit](https://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm)

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

        """
        if segments == -1:
            segments = Config.get_default_segments()
        _chkGE("segments", segments, 3)

        if join_type == "round":
            jt = _m.JoinType.Round
        elif join_type == "square":
            jt = _m.JoinType.Square
        elif join_type == "miter":
            jt = _m.JoinType.Miter
        else:
            raise ValidationError(
                'Invalid join type specified, must be one of: "round", "square", or "miter"'
            )
        o2 = Obj2d(self.mo.offset(delta, jt, miter_limit, segments))
        o2._color = self._color
        return o2

    def piecut(
        self, start_angle=0, end_angle=90, both=False
    ) -> Obj2d | tuple[Obj2d, Obj2d]:
        """
        Cut a wedge out of this object.

        It returns a copy of this object minus the wedge.

        Or if `both` is `True` it returns a tuple of the object minus the wedge and the object intersected with the wedge.
        """
        if end_angle < start_angle:
            end_angle = end_angle + 360.0
        x1, y1, x2, y2 = self.bounding_box()
        c_x = (x2 + x1) / 2.0
        c_y = (y2 + y1) / 2.0
        rad = max(x2 - x1, y2 - y1) * 4
        pts = []
        if end_angle - start_angle != 180.0:
            pts.append((c_x, c_y))
        pts.append((rad * cos(start_angle) + c_x, rad * sin(start_angle) + c_y))
        ang = 90 + start_angle
        while ang < end_angle:
            pts.append((rad * cos(ang) + c_x, rad * sin(ang) + c_y))
            ang = ang + 90
        pts.append((rad * cos(end_angle) + c_x, rad * sin(end_angle) + c_y))
        cutter = Obj2d(_m.CrossSection([pts]))
        o1 = difference(self, cutter)
        o1._color = self._color
        if both:
            o2 = intersect(self, cutter)
            o2._color = self._color
            return (o1, o2)
        return o1

    def resize(self, sizes: list[tuple[float | None, float | None]]) -> Obj2d:
        """
        Resize this object to a specific size on each axis.

        If an axis size is specified as zero, it is automatically calculated by the other specified size to
        maintain the same size ratio between the two sizes.

        If an axis size is specified as `None`, no change is made to that axis.
        """
        _chkV2("sizes", sizes)
        if (sizes[0] == None or sizes[0] == 0) and (sizes[1] == None or sizes[1] == 0):
            return self.scale((1, 1))

        min_x, min_y, max_x, max_y = self.bounding_box()
        self_sizes = [max_x - min_x, max_y - min_y]

        sf = [1, 1]
        for i in range(len(sizes)):
            if sizes[i] == None:
                continue
            if sizes[i] == 0:
                continue
            sf[i] = sizes[i] / self_sizes[i]
            if sizes[(i + 1) % 2] == 0:
                sf[(i + 1) % 2] = sf[i]
        return self.scale(sf)

    def revolve(self, revolve_degrees: float = 360.0, segments: int = -1) -> Obj3d:
        """
        Create an Obj3d by revolving this object around the Y-axis, then rotating it so that Y becomes Z.

        For `segments` see the documentation of [`Config.set_default_segments`](index.html#piecad.Config.set_default_segments).
        """
        o3 = revolve(self, revolve_degrees, segments)
        return o3

    def rotate(self, degrees: float) -> Obj2d:
        """
        Rotate this object by the given degrees.

        """
        return Obj2d(self.mo.rotate(degrees), color=self._color)

    def scale(self, factors: list[float, float]) -> Obj2d:
        """
        Scale this object by the given factors.

        If you want no change, use `1.0`, that means 100% (thus unchanged).
        """
        _chkV2("factors", factors)
        return Obj2d(self.mo.scale(factors), color=self._color)

    def simplify(self, tolerance: float = 0) -> Obj2d:
        """
        Return a simplified version of this object.

        Using a tolerance of 0 means that the current tolerance will be used.
        """
        _chkGE("tolerance", tolerance, 0)
        return Obj2d(self.mo.simplify(tolerance))

    def to_paths(self) -> list[list[float, float]]:
        """
        Return a list of paths, each of which is a list of vertices that make up this object.
        """
        from copy import deepcopy

        paths = self.mo.to_polygons()
        paths = deepcopy(paths)
        paths = [[tuple(float(coord) for coord in pt) for pt in path] for path in paths]
        return paths

    def transform(
        self, matrix2x3: tuple[tuple[float, float, float], tuple[float, float, float]]
    ) -> Obj2d:
        """
        Transform this object with the given affine transformation matrix.

        If you don't know what this is, you probably don't need it.
        """
        if len(matrix2x3) != 2 or len(matrix2x3[0]) != 3 or len(matrix2x3[1]) != 3:
            raise ValueError("Improperly sized 2x3 matrix.")

        return Obj2d(self.mo.transform(matrix2x3), color=self._color)

    def translate(self, offsets: list[float, float]) -> Obj2d:
        """
        Translate (move) this object by the given offsets.
        """
        _chkV2("offsets", offsets)
        return Obj2d(self.mo.translate(offsets), color=self._color)

Wrapper class for "CrossSections", which are 2D graphical objects.

Attributes -----= mo The Manifold::CrossSection object used by manifold3d.

Methods

def area(self) ‑> float
Expand source code
def area(self) -> float:
    """
    The area of this Obj2d.
    """
    return self.mo.area()

The area of this Obj2d.

def bounding_box(self) ‑> tuple[float, float, float, float]
Expand source code
def bounding_box(self) -> tuple[float, float, float, float]:
    """
    Return the bounding box of this object.

    Return a tuple: (left, bottom, right, top) which represents
    a rectangle that would exactly contain this object.

    It can be broken up like this: `x1, y1, x2, y2 = obj.bounding_box()`
    """
    return self.mo.bounds()

Return the bounding box of this object.

Return a tuple: (left, bottom, right, top) which represents a rectangle that would exactly contain this object.

It can be broken up like this: x1, y1, x2, y2 = obj.bounding_box()

def center(self, axes: tuple[bool, bool] = (True, True), at: tuple[float, float] = (0, 0)) ‑> Obj2d
Expand source code
def center(
    self, axes: tuple[bool, bool] = (True, True), at: tuple[float, float] = (0, 0)
) -> Obj2d:
    """
    Center the object on each of the `True` axes.

    Parameter `at` specifies the point to center on.

    """
    xmin, ymin, xmax, ymax = self.bounding_box()
    mid_x = (xmax - xmin) / 2 + xmin
    mid_y = (ymax - ymin) / 2 + ymin
    new_x = 0
    new_y = 0
    if axes[0]:
        new_x = at[0] - mid_x
    if axes[1]:
        new_y = at[1] - mid_y
    o2 = self.translate((new_x, new_y))
    o2._color = self._color
    return o2

Center the object on each of the True axes.

Parameter at specifies the point to center on.

def color(self, cspec: tuple[int, int, int] | str) ‑> Obj2d
Expand source code
def color(self, cspec: tuple[int, int, int] | str) -> Obj2d:
    """
    Assign the given color to this object.</summary>

    Parameters:

        The `cspec` parameter has 3 formats:

        *   A tuple of RGB color values, where each value is between
            0 and 255. Such as: `(255, 128, 0)`

        * A string beginning with "#" followed by 6 hex digits
            representing RGB. Such as "#FF00FF"
        * A string that is one of the basic or extended CSS color names.
          For a list of color names see: [Color keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords)
    """
    return Obj2d(self.mo, _parse_color(cspec))

Assign the given color to this object.

Parameters -----=

The cspec parameter has 3 formats:

  • A tuple of RGB color values, where each value is between 0 and 255. Such as: (255, 128, 0)

  • A string beginning with "#" followed by 6 hex digits representing RGB. Such as "#FF00FF"

  • A string that is one of the basic or extended CSS color names. For a list of color names see: Color keywords
def corner(self, at: tuple[float, float] = (0, 0)) ‑> Obj2d
Expand source code
def corner(self, at: tuple[float, float] = (0, 0)) -> Obj2d:
    """
    Move the bounding box minimum corner to (0, 0).

    Parameter `at` specifies an alternate point to move the corner to.
    """
    xmin, ymin, xmax, ymax = self.bounding_box()
    new_x = at[0] - xmin
    new_y = at[1] - ymin
    o2 = self.translate((new_x, new_y))
    o2._color = self._color
    return o2

Move the bounding box minimum corner to (0, 0).

Parameter at specifies an alternate point to move the corner to.

def decompose(self) ‑> list[Obj2d]
Expand source code
def decompose(self) -> list[Obj2d]:
    """
    Decompose this object into a list of topologically disjoint objects.

    """
    ml = self.mo.decompose()
    l = []
    for m in ml:
        l.append(Obj2d(m, color=self._color))
    return l

Decompose this object into a list of topologically disjoint objects.

def extrude(self, height: int) ‑> Obj3d
Expand source code
def extrude(self, height: int) -> Obj3d:
    """
    Extrude this object into a Obj3d of the given height.
    """
    o3 = Obj3d(_m.Manifold.extrude(self.mo, height))
    return o3

Extrude this object into a Obj3d of the given height.

def is_empty(self) ‑> bool
Expand source code
def is_empty(self) -> bool:
    """
    Is this object empty?
    """
    return self.mo.is_empty()

Is this object empty?

def mirror(self, axes: tuple[bool, bool]) ‑> Obj2d
Expand source code
def mirror(self, axes: tuple[bool, bool]) -> Obj2d:
    """
    Mirror this object around the given axes.

    To understand this, pretend you are holding you right hand up in front of a mirror.
    Consider the hand in mirror the original image, but flattened to 2D.
    Mirror over the x axis and you will get an image to the left that looks like a left hand.
    Mirror over the y axis and you will get an image of the right hand upside down.
    Mirror over the x and y axes and you will get an image a left hand upside down.

    From a mathematical standpoint, mirroring negates all points on each selected axis.

    """
    uv = [0, 0]
    if axes[0]:
        uv[0] = 1
    if axes[1]:
        uv[1] = 1
    return Obj2d(self.mo.mirror(uv), self._color)

Mirror this object around the given axes.

To understand this, pretend you are holding you right hand up in front of a mirror. Consider the hand in mirror the original image, but flattened to 2D. Mirror over the x axis and you will get an image to the left that looks like a left hand. Mirror over the y axis and you will get an image of the right hand upside down. Mirror over the x and y axes and you will get an image a left hand upside down.

From a mathematical standpoint, mirroring negates all points on each selected axis.

def num_verts(self) ‑> int
Expand source code
def num_verts(self) -> int:
    """
    The number of vertices in this object.

    This is useful in unit testing shapes.

    It is very difficult to check that a shape is correct, but knowing that a correct
    number of vertices is present goes a long way to making sure things are healthy.
    """
    return self.mo.num_vert()

The number of vertices in this object.

This is useful in unit testing shapes.

It is very difficult to check that a shape is correct, but knowing that a correct number of vertices is present goes a long way to making sure things are healthy.

def offset(self, delta: float, join_type: str, miter_limit: float = 2.0, segments: int = -1) ‑> Obj2d
Expand source code
def offset(
    self, delta: float, join_type: str, miter_limit: float = 2.0, segments: int = -1
) -> Obj2d:
    """
    Offset (or inset) a 2D object by a given distance called `delta`.
    A negative delta will cause an inset to be done.

    Select `join_type` from `"square"`, `"round"`, or `"miter"`.
    To understand `miter_limit`, see: [Clipper2 MiterLimit](https://www.angusj.com/clipper2/Docs/Units/Clipper.Offset/Classes/ClipperOffset/Properties/MiterLimit.htm)

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

    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGE("segments", segments, 3)

    if join_type == "round":
        jt = _m.JoinType.Round
    elif join_type == "square":
        jt = _m.JoinType.Square
    elif join_type == "miter":
        jt = _m.JoinType.Miter
    else:
        raise ValidationError(
            'Invalid join type specified, must be one of: "round", "square", or "miter"'
        )
    o2 = Obj2d(self.mo.offset(delta, jt, miter_limit, segments))
    o2._color = self._color
    return o2

Offset (or inset) a 2D object by a given distance called delta. A negative delta will cause an inset to be done.

Select join_type from "square", "round", or "miter". To understand miter_limit, see: Clipper2 MiterLimit

For segments see the documentation of Config.set_default_segments.

def piecut(self, start_angle=0, end_angle=90, both=False) ‑> Obj2d | tuple[Obj2d, Obj2d]
Expand source code
def piecut(
    self, start_angle=0, end_angle=90, both=False
) -> Obj2d | tuple[Obj2d, Obj2d]:
    """
    Cut a wedge out of this object.

    It returns a copy of this object minus the wedge.

    Or if `both` is `True` it returns a tuple of the object minus the wedge and the object intersected with the wedge.
    """
    if end_angle < start_angle:
        end_angle = end_angle + 360.0
    x1, y1, x2, y2 = self.bounding_box()
    c_x = (x2 + x1) / 2.0
    c_y = (y2 + y1) / 2.0
    rad = max(x2 - x1, y2 - y1) * 4
    pts = []
    if end_angle - start_angle != 180.0:
        pts.append((c_x, c_y))
    pts.append((rad * cos(start_angle) + c_x, rad * sin(start_angle) + c_y))
    ang = 90 + start_angle
    while ang < end_angle:
        pts.append((rad * cos(ang) + c_x, rad * sin(ang) + c_y))
        ang = ang + 90
    pts.append((rad * cos(end_angle) + c_x, rad * sin(end_angle) + c_y))
    cutter = Obj2d(_m.CrossSection([pts]))
    o1 = difference(self, cutter)
    o1._color = self._color
    if both:
        o2 = intersect(self, cutter)
        o2._color = self._color
        return (o1, o2)
    return o1

Cut a wedge out of this object.

It returns a copy of this object minus the wedge.

Or if both is True it returns a tuple of the object minus the wedge and the object intersected with the wedge.

def resize(self, sizes: list[tuple[float | None, float | None]]) ‑> Obj2d
Expand source code
def resize(self, sizes: list[tuple[float | None, float | None]]) -> Obj2d:
    """
    Resize this object to a specific size on each axis.

    If an axis size is specified as zero, it is automatically calculated by the other specified size to
    maintain the same size ratio between the two sizes.

    If an axis size is specified as `None`, no change is made to that axis.
    """
    _chkV2("sizes", sizes)
    if (sizes[0] == None or sizes[0] == 0) and (sizes[1] == None or sizes[1] == 0):
        return self.scale((1, 1))

    min_x, min_y, max_x, max_y = self.bounding_box()
    self_sizes = [max_x - min_x, max_y - min_y]

    sf = [1, 1]
    for i in range(len(sizes)):
        if sizes[i] == None:
            continue
        if sizes[i] == 0:
            continue
        sf[i] = sizes[i] / self_sizes[i]
        if sizes[(i + 1) % 2] == 0:
            sf[(i + 1) % 2] = sf[i]
    return self.scale(sf)

Resize this object to a specific size on each axis.

If an axis size is specified as zero, it is automatically calculated by the other specified size to maintain the same size ratio between the two sizes.

If an axis size is specified as None, no change is made to that axis.

def revolve(self, revolve_degrees: float = 360.0, segments: int = -1) ‑> Obj3d
Expand source code
def revolve(self, revolve_degrees: float = 360.0, segments: int = -1) -> Obj3d:
    """
    Create an Obj3d by revolving this object around the Y-axis, then rotating it so that Y becomes Z.

    For `segments` see the documentation of [`Config.set_default_segments`](index.html#piecad.Config.set_default_segments).
    """
    o3 = revolve(self, revolve_degrees, segments)
    return o3

Create an Obj3d by revolving this object around the Y-axis, then rotating it so that Y becomes Z.

For segments see the documentation of Config.set_default_segments.

def rotate(self, degrees: float) ‑> Obj2d
Expand source code
def rotate(self, degrees: float) -> Obj2d:
    """
    Rotate this object by the given degrees.

    """
    return Obj2d(self.mo.rotate(degrees), color=self._color)

Rotate this object by the given degrees.

def scale(self, factors: list[float, float]) ‑> Obj2d
Expand source code
def scale(self, factors: list[float, float]) -> Obj2d:
    """
    Scale this object by the given factors.

    If you want no change, use `1.0`, that means 100% (thus unchanged).
    """
    _chkV2("factors", factors)
    return Obj2d(self.mo.scale(factors), color=self._color)

Scale this object by the given factors.

If you want no change, use 1.0, that means 100% (thus unchanged).

def simplify(self, tolerance: float = 0) ‑> Obj2d
Expand source code
def simplify(self, tolerance: float = 0) -> Obj2d:
    """
    Return a simplified version of this object.

    Using a tolerance of 0 means that the current tolerance will be used.
    """
    _chkGE("tolerance", tolerance, 0)
    return Obj2d(self.mo.simplify(tolerance))

Return a simplified version of this object.

Using a tolerance of 0 means that the current tolerance will be used.

def to_paths(self) ‑> list[list[float, float]]
Expand source code
def to_paths(self) -> list[list[float, float]]:
    """
    Return a list of paths, each of which is a list of vertices that make up this object.
    """
    from copy import deepcopy

    paths = self.mo.to_polygons()
    paths = deepcopy(paths)
    paths = [[tuple(float(coord) for coord in pt) for pt in path] for path in paths]
    return paths

Return a list of paths, each of which is a list of vertices that make up this object.

def transform(self,
matrix2x3: tuple[tuple[float, float, float], tuple[float, float, float]]) ‑> Obj2d
Expand source code
def transform(
    self, matrix2x3: tuple[tuple[float, float, float], tuple[float, float, float]]
) -> Obj2d:
    """
    Transform this object with the given affine transformation matrix.

    If you don't know what this is, you probably don't need it.
    """
    if len(matrix2x3) != 2 or len(matrix2x3[0]) != 3 or len(matrix2x3[1]) != 3:
        raise ValueError("Improperly sized 2x3 matrix.")

    return Obj2d(self.mo.transform(matrix2x3), color=self._color)

Transform this object with the given affine transformation matrix.

If you don't know what this is, you probably don't need it.

def translate(self, offsets: list[float, float]) ‑> Obj2d
Expand source code
def translate(self, offsets: list[float, float]) -> Obj2d:
    """
    Translate (move) this object by the given offsets.
    """
    _chkV2("offsets", offsets)
    return Obj2d(self.mo.translate(offsets), color=self._color)

Translate (move) this object by the given offsets.

class Obj3d (o: object = None)
Expand source code
class Obj3d:
    """
    Wrapper class for "Manifolds", which are 3D graphical objects.

    Attributes:
        mo The Manifold::Manifold object used by manifold3d.
    """

    color_map = {}

    def __init__(self, o: object = None) -> None:
        if o == None:
            o = _m.Manifold().as_original()
        self.mo = o

    def bounding_box(self) -> tuple[float, float, float, float, float, float]:
        """
        Return the bounding box of this object.

        Return a tuple: (left, front, bottom, right, back, top) which represents
        a cuboid that would exactly contain this object.

        It can be broken up like this: `x1, y1, z1, x2, y2, z2 = obj.bounding_box()`
        """
        return self.mo.bounding_box()

    def center(
        self,
        axes: tuple[bool, bool, bool] = (True, True, True),
        at: tuple[float, float, float] = (0, 0, 0),
    ) -> Obj3d:
        """
        Center the object on each of the `True` axes.

        Parameter `at` specifies the point to center on.

        """
        xmin, ymin, zmin, xmax, ymax, zmax = self.bounding_box()
        mid_x = (xmax - xmin) / 2 + xmin
        mid_y = (ymax - ymin) / 2 + ymin
        mid_z = (zmax - zmin) / 2 + zmin
        new_x = 0
        new_y = 0
        new_z = 0
        if axes[0]:
            new_x = at[0] - mid_x
        if axes[1]:
            new_y = at[1] - mid_y
        if axes[2]:
            new_z = at[2] - mid_z
        o3 = self.translate((new_x, new_y, new_z))
        return o3

    def color(self, cspec: tuple[int, int, int] | str) -> Obj3d:
        """
        Assign the given color to this object.</summary>

        Parameters:

            The `cspec` parameter has 3 formats:

            *   A tuple of RGB color values, where each value is between
                0 and 255. Such as: `(255, 128, 0)`

            * A string beginning with "#" followed by 6 hex digits
                representing RGB. Such as "#FF00FF"
            * A string that is one of the basic or extended CSS color names.
              For a list of color names see: [Color keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords)
        """
        mo = self.mo.as_original()
        Obj3d.color_map[mo.original_id()] = _parse_color(cspec)
        return Obj3d(mo)

    def corner(
        self,
        at: tuple[float, float, float] = (0, 0, 0),
    ) -> Obj3d:
        """
        Move the bounding box minimum corner to (0, 0, 0).

        Parameter `at` specifies an alternate point to move the corner to.
        """
        xmin, ymin, zmin, xmax, ymax, zmax = self.bounding_box()
        new_x = at[0] - xmin
        new_y = at[1] - ymin
        new_z = at[2] - zmin
        o3 = self.translate((new_x, new_y, new_z))
        return o3

    def decompose(self) -> list[Obj3d]:
        """
        Decompose this object into a list of topologically disjoint objects.

        """
        ml = self.mo.decompose()
        l = []
        for m in ml:
            l.append(Obj3d(m))
        return l

    def is_empty(self) -> bool:
        """
        Is this object empty?

        """
        return self.mo.is_empty()

    def minkowski_difference(self, other: Obj3d) -> Obj3d:
        """
        Return the minkowski_sum of this object and other.
        The mikowski_sum is formed by sweeping the other object along the boundary of this object
        subtracting it.
        """
        return Obj3d(self.mo.minkowski_difference(other.mo))

    def minkowski_sum(self, other: Obj3d) -> Obj3d:
        """
        Return the minkowski_sum of this object and other.
        The mikowski_sum is formed by sweeping the other object along the bound of this object
        adding it.

        <iframe width="100%" height="250" src="examples/minkowski_sum.html"></iframe>
        """
        return Obj3d(self.mo.minkowski_sum(other.mo))

    def mirror(self, axes: tuple[bool, bool, bool]) -> Obj3d:
        """
        Mirror this object around the given axes.

        To understand this, pretend you are holding you right hand up in front of a mirror.
        Consider the hand in mirror the original image.
        Mirror over the x axis and you will get an image to the left that looks like a left hand.
        Mirror over the y axis and you will get an image like the back of the hand you are holding up.
        Mirror over the x and y axes and you will get an image like the back of a left hand.
        For the Z axis it's the same, but each hand is upside down.

        From a mathematical standpoint, mirroring negates all points on each selected axis.

        """
        uv = [0, 0, 0]
        if axes[0]:
            uv[0] = 1
        if axes[1]:
            uv[1] = 1
        if axes[2]:
            uv[2] = 1
        return Obj3d(self.mo.mirror(uv))

    def miter_cut(
        self, cut_angle: float, cut_point: tuple[float, float, float]
    ) -> tuple[Obj3d, Obj3d]:
        """
        This works similarly to a miter saw.
        It cuts this object into two parts using a plane defined by the `cut_angle` and `cut_point`.
        Cut_angle is in degrees, and cut_point is a point through which the cut plane passes.
        The cut is parallel to the Y axis, with the angle defining the slope of the cut.
        The two parts are returned as a tuple of Obj3d objects.
        The returned order is the one with the least x, or then the least z, or then the least volume.
        """

        _chkNum("cut_angle", cut_angle)
        if cut_angle < -90 or cut_angle > 90:
            raise ValidationError(
                "Parameter cut_angle must be between -90 and 90 degrees."
            )

        _chkV3("cut_point", cut_point)
        for value in cut_point:
            _chkNum("cut_point", value)

        # A direction vector in the X-Z plane at cut_angle.
        cut_direction = (cos(cut_angle), 0.0, sin(cut_angle))

        # The plane is parallel to the Y-axis.
        y_direction = (0.0, 1.0, 0.0)

        # Plane normal.
        normal = Vec3.cross(y_direction, cut_direction)

        # Plane equation:
        #     normal · point + offset = 0
        # The plane passes through cut_point.
        offset = Vec3.dot(normal, cut_point)

        first, second = self.mo.split_by_plane(normal, offset)
        fxmin, fymin, fzmin, fxmax, fymax, fzmax = first.bounding_box()
        sxmin, symin, szmin, sxmax, symax, szmax = second.bounding_box()
        # Minimums closer than epsilon are ties, so numerical noise cannot decide the order.
        if not near_eq(fxmin, sxmin):
            first_is_first = fxmin < sxmin
        elif not near_eq(fzmin, szmin):
            first_is_first = fzmin < szmin
        else:
            first_is_first = first.volume() <= second.volume() + Const.epsilon
        if first_is_first:
            return Obj3d(first), Obj3d(second)
        else:
            return Obj3d(second), Obj3d(first)

    def num_faces(self) -> int:
        """
        The number of faces in this object.

        This is useful in unit testing shapes.

        It is very difficult to check that a shape is correct, but knowing that a correct
        number of faces is present goes a long way to making sure things are healthy.
        """
        return self.mo.num_tri()

    def num_verts(self) -> int:
        """
        The number of vertices in this object.

        This is useful in unit testing shapes.

        It is very difficult to check that a shape is correct, but knowing that a correct
        number of vertices is present goes a long way to making sure things are healthy.
        """
        return self.mo.num_vert()

    def piecut(
        self, start_angle=0, end_angle=90, both=False
    ) -> Obj3d | tuple[Obj3d, Obj3d]:
        """
        Cut a wedge out of this object.

        It returns a copy of this object minus the wedge.

        Or if `both` is `True` it returns a tuple of the object minus the wedge and the object intersected with the wedge.

        """
        if end_angle < start_angle:
            end_angle = end_angle + 360.0
        if end_angle - start_angle >= 360.0:
            raise ValidationError(
                "Parameters start_angle and end_angle must be less than 360 degrees apart."
            )
        x1, y1, z1, x2, y2, z2 = self.bounding_box()
        c_x = (x2 + x1) / 2.0
        c_y = (y2 + y1) / 2.0
        c_z = (z2 + z1) / 2.0
        rad = max(z2 - z1, x2 - x1, y2 - y1) * 4
        h = z2 - z1
        pts = []
        if end_angle - start_angle != 180.0:
            pts.append((c_x, c_y))
        pts.append((rad * cos(start_angle) + c_x, rad * sin(start_angle) + c_y))
        ang = 90 + start_angle
        while ang < end_angle:
            pts.append((rad * cos(ang) + c_x, rad * sin(ang) + c_y))
            ang = ang + 90
        pts.append((rad * cos(end_angle) + c_x, rad * sin(end_angle) + c_y))
        cutter = Obj3d(_m.Manifold.extrude(_m.CrossSection([pts]), h)).translate(
            (0, 0, z1)
        )
        if both:
            o2, o1 = self.split(cutter)
            return (o1, o2)
        o3 = difference(self, cutter)
        return o3

    def project(self) -> Obj2d:
        """
        Return a Obj2d representing this object's "shadow" on the x-y plane.

        """
        return Obj2d(self.mo.project())

    def resize(
        self, sizes: list[tuple[float | None, float | None, float | None]]
    ) -> Obj3d:
        """
        Resize this object to a specific size on each axis.

        If an axis size is specified as zero, it is automatically calculated by a specified size to
        maintain the same size ratio between the two sizes.

        If an axis size is specified as `None`, no change is made to that axis.
        """
        _chkV3("sizes", sizes)
        if (
            (sizes[0] == None or sizes[0] == 0)
            and (sizes[1] == None or sizes[1] == 0)
            and (sizes[2] == None or sizes[2] == 0)
        ):
            return self.scale((1, 1, 1))

        min_x, min_y, min_z, max_x, max_y, max_z = self.bounding_box()
        self_sizes = [max_x - min_x, max_y - min_y, max_z - min_z]

        auto = False
        for i in range(len(sizes)):
            if sizes[i] == 0:
                auto = True
                break
        sf = [1, 1, 1]
        scale = 1
        scaled_cnt = 0
        for i in range(len(sizes)):
            if sizes[i] == None:
                continue
            if sizes[i] == 0:
                continue
            scale = sf[i] = sizes[i] / self_sizes[i]
            scaled_cnt = scaled_cnt + 1
        if auto and scaled_cnt != 1:
            raise ValidationError(
                "If you're using auto, then only one size can be specified."
            )
        for i in range(len(sizes)):
            if sizes[i] == 0:
                sf[i] = scale
        return self.scale(sf)

    def rotate(self, degrees: list[float, float, float]) -> Obj3d:
        """
        Rotate this object by the given degrees for each axis.

        """
        _chkV3("degrees", degrees)
        return Obj3d(self.mo.rotate(degrees))

    def scale(self, factors: list[float, float, float]) -> Obj3d:
        """
        Scale this object by the given factors.

        If you want no change, use `1.0`, that means 100% (thus unchanged).
        """
        _chkV3("factors", factors)
        return Obj3d(self.mo.scale(factors))

    def simplify(self, tolerance: float = 0) -> Obj3d:
        """
        Return a simplified version of this object.

        Using a tolerance of 0 means that the current tolerance will be used.
        """
        _chkGE("tolerance", tolerance, 0)
        return Obj3d(self.mo.simplify(tolerance))

    def slice(self, height: float) -> Obj2d:
        """
        Like `project`, but rather than the bottom, project at the given height.

        """
        _chkGT("height", height, 0)
        return Obj2d(self.mo.slice(height))

    def split(self, cutter: Obj3d) -> Obj3d:
        """
        This performs difference and intersection operations between this object and the cutter more efficiently.

        The return value is `(inter_obj, diff_obj)`.

        """
        ret = self.mo.split(cutter.mo)
        return (Obj3d(ret[0]), Obj3d(ret[1]))

    def surface_area(self) -> float:
        """
        The surface area of this Obj3d.
        """
        return self.mo.surface_area()

    def to_verts_and_faces(
        self, np: bool = False
    ) -> tuple[list[tuple[float, float, float]], list[tuple[int, int, int]]]:
        """
        Return a pair containing a list of vertices and a list of faces for this object.

        Manifold3d uses numpy arrays to avoid an array copy. For most uses, python lists of tuples are
        more efficient. However, if you are using numpy for further processing, you may want to set `np=True`,
        to preserve the arrays as numpy arrays.
        Numpy is faster at vectorizable operations, but is often 10x slower for smaller operations such as `cross` and `dot`,
        so you may want to do timing tests.

        """
        mesh = self.mo.to_mesh64()
        if mesh.vert_properties.shape[1] > 3:
            vertices = mesh.vert_properties[:, :3]
        else:
            vertices = mesh.vert_properties
        if np:
            return (vertices, mesh.tri_verts)
        v = list(map(tuple, vertices.tolist()))
        f = list(map(tuple, mesh.tri_verts.tolist()))
        return (v, f)

    def transform(
        self,
        matrix3x4: tuple[
            tuple[float, float, float, float],
            tuple[float, float, float, float],
            tuple[float, float, float, float],
            tuple[float, float, float, float],
        ],
    ) -> Obj3d:
        """
        Transform this object with the given affine transformation matrix.

        If you don't know what this is, you probably don't need it.
        """
        if (
            len(matrix3x4) != 3
            or len(matrix3x4[0]) != 4
            or len(matrix3x4[1]) != 4
            or len(matrix3x4[2]) != 4
        ):
            raise ValueError("Improperly sized 3x4 matrix.")

        return Obj3d(self.mo.transform(matrix3x4))

    def translate(self, offsets: list[float, float, float]) -> Obj3d:
        """
        Translate (move) this object by the given offsets.
        """
        _chkV3("offsets", offsets)
        return Obj3d(self.mo.translate(offsets))

    def volume(self) -> float:
        """
        The volume of this Obj3d.
        """
        return self.mo.volume()

Wrapper class for "Manifolds", which are 3D graphical objects.

Attributes -----= mo The Manifold::Manifold object used by manifold3d.

Class variables

var color_map

Methods

def bounding_box(self) ‑> tuple[float, float, float, float, float, float]
Expand source code
def bounding_box(self) -> tuple[float, float, float, float, float, float]:
    """
    Return the bounding box of this object.

    Return a tuple: (left, front, bottom, right, back, top) which represents
    a cuboid that would exactly contain this object.

    It can be broken up like this: `x1, y1, z1, x2, y2, z2 = obj.bounding_box()`
    """
    return self.mo.bounding_box()

Return the bounding box of this object.

Return a tuple: (left, front, bottom, right, back, top) which represents a cuboid that would exactly contain this object.

It can be broken up like this: x1, y1, z1, x2, y2, z2 = obj.bounding_box()

def center(self,
axes: tuple[bool, bool, bool] = (True, True, True),
at: tuple[float, float, float] = (0, 0, 0)) ‑> Obj3d
Expand source code
def center(
    self,
    axes: tuple[bool, bool, bool] = (True, True, True),
    at: tuple[float, float, float] = (0, 0, 0),
) -> Obj3d:
    """
    Center the object on each of the `True` axes.

    Parameter `at` specifies the point to center on.

    """
    xmin, ymin, zmin, xmax, ymax, zmax = self.bounding_box()
    mid_x = (xmax - xmin) / 2 + xmin
    mid_y = (ymax - ymin) / 2 + ymin
    mid_z = (zmax - zmin) / 2 + zmin
    new_x = 0
    new_y = 0
    new_z = 0
    if axes[0]:
        new_x = at[0] - mid_x
    if axes[1]:
        new_y = at[1] - mid_y
    if axes[2]:
        new_z = at[2] - mid_z
    o3 = self.translate((new_x, new_y, new_z))
    return o3

Center the object on each of the True axes.

Parameter at specifies the point to center on.

def color(self, cspec: tuple[int, int, int] | str) ‑> Obj3d
Expand source code
def color(self, cspec: tuple[int, int, int] | str) -> Obj3d:
    """
    Assign the given color to this object.</summary>

    Parameters:

        The `cspec` parameter has 3 formats:

        *   A tuple of RGB color values, where each value is between
            0 and 255. Such as: `(255, 128, 0)`

        * A string beginning with "#" followed by 6 hex digits
            representing RGB. Such as "#FF00FF"
        * A string that is one of the basic or extended CSS color names.
          For a list of color names see: [Color keywords](https://www.w3.org/wiki/CSS/Properties/color/keywords)
    """
    mo = self.mo.as_original()
    Obj3d.color_map[mo.original_id()] = _parse_color(cspec)
    return Obj3d(mo)

Assign the given color to this object.

Parameters -----=

The cspec parameter has 3 formats:

  • A tuple of RGB color values, where each value is between 0 and 255. Such as: (255, 128, 0)

  • A string beginning with "#" followed by 6 hex digits representing RGB. Such as "#FF00FF"

  • A string that is one of the basic or extended CSS color names. For a list of color names see: Color keywords
def corner(self, at: tuple[float, float, float] = (0, 0, 0)) ‑> Obj3d
Expand source code
def corner(
    self,
    at: tuple[float, float, float] = (0, 0, 0),
) -> Obj3d:
    """
    Move the bounding box minimum corner to (0, 0, 0).

    Parameter `at` specifies an alternate point to move the corner to.
    """
    xmin, ymin, zmin, xmax, ymax, zmax = self.bounding_box()
    new_x = at[0] - xmin
    new_y = at[1] - ymin
    new_z = at[2] - zmin
    o3 = self.translate((new_x, new_y, new_z))
    return o3

Move the bounding box minimum corner to (0, 0, 0).

Parameter at specifies an alternate point to move the corner to.

def decompose(self) ‑> list[Obj3d]
Expand source code
def decompose(self) -> list[Obj3d]:
    """
    Decompose this object into a list of topologically disjoint objects.

    """
    ml = self.mo.decompose()
    l = []
    for m in ml:
        l.append(Obj3d(m))
    return l

Decompose this object into a list of topologically disjoint objects.

def is_empty(self) ‑> bool
Expand source code
def is_empty(self) -> bool:
    """
    Is this object empty?

    """
    return self.mo.is_empty()

Is this object empty?

def minkowski_difference(self, other: Obj3d) ‑> Obj3d
Expand source code
def minkowski_difference(self, other: Obj3d) -> Obj3d:
    """
    Return the minkowski_sum of this object and other.
    The mikowski_sum is formed by sweeping the other object along the boundary of this object
    subtracting it.
    """
    return Obj3d(self.mo.minkowski_difference(other.mo))

Return the minkowski_sum of this object and other. The mikowski_sum is formed by sweeping the other object along the boundary of this object subtracting it.

def minkowski_sum(self, other: Obj3d) ‑> Obj3d
Expand source code
def minkowski_sum(self, other: Obj3d) -> Obj3d:
    """
    Return the minkowski_sum of this object and other.
    The mikowski_sum is formed by sweeping the other object along the bound of this object
    adding it.

    <iframe width="100%" height="250" src="examples/minkowski_sum.html"></iframe>
    """
    return Obj3d(self.mo.minkowski_sum(other.mo))

Return the minkowski_sum of this object and other. The mikowski_sum is formed by sweeping the other object along the bound of this object adding it.

def mirror(self, axes: tuple[bool, bool, bool]) ‑> Obj3d
Expand source code
def mirror(self, axes: tuple[bool, bool, bool]) -> Obj3d:
    """
    Mirror this object around the given axes.

    To understand this, pretend you are holding you right hand up in front of a mirror.
    Consider the hand in mirror the original image.
    Mirror over the x axis and you will get an image to the left that looks like a left hand.
    Mirror over the y axis and you will get an image like the back of the hand you are holding up.
    Mirror over the x and y axes and you will get an image like the back of a left hand.
    For the Z axis it's the same, but each hand is upside down.

    From a mathematical standpoint, mirroring negates all points on each selected axis.

    """
    uv = [0, 0, 0]
    if axes[0]:
        uv[0] = 1
    if axes[1]:
        uv[1] = 1
    if axes[2]:
        uv[2] = 1
    return Obj3d(self.mo.mirror(uv))

Mirror this object around the given axes.

To understand this, pretend you are holding you right hand up in front of a mirror. Consider the hand in mirror the original image. Mirror over the x axis and you will get an image to the left that looks like a left hand. Mirror over the y axis and you will get an image like the back of the hand you are holding up. Mirror over the x and y axes and you will get an image like the back of a left hand. For the Z axis it's the same, but each hand is upside down.

From a mathematical standpoint, mirroring negates all points on each selected axis.

def miter_cut(self, cut_angle: float, cut_point: tuple[float, float, float]) ‑> tuple[Obj3d, Obj3d]
Expand source code
def miter_cut(
    self, cut_angle: float, cut_point: tuple[float, float, float]
) -> tuple[Obj3d, Obj3d]:
    """
    This works similarly to a miter saw.
    It cuts this object into two parts using a plane defined by the `cut_angle` and `cut_point`.
    Cut_angle is in degrees, and cut_point is a point through which the cut plane passes.
    The cut is parallel to the Y axis, with the angle defining the slope of the cut.
    The two parts are returned as a tuple of Obj3d objects.
    The returned order is the one with the least x, or then the least z, or then the least volume.
    """

    _chkNum("cut_angle", cut_angle)
    if cut_angle < -90 or cut_angle > 90:
        raise ValidationError(
            "Parameter cut_angle must be between -90 and 90 degrees."
        )

    _chkV3("cut_point", cut_point)
    for value in cut_point:
        _chkNum("cut_point", value)

    # A direction vector in the X-Z plane at cut_angle.
    cut_direction = (cos(cut_angle), 0.0, sin(cut_angle))

    # The plane is parallel to the Y-axis.
    y_direction = (0.0, 1.0, 0.0)

    # Plane normal.
    normal = Vec3.cross(y_direction, cut_direction)

    # Plane equation:
    #     normal · point + offset = 0
    # The plane passes through cut_point.
    offset = Vec3.dot(normal, cut_point)

    first, second = self.mo.split_by_plane(normal, offset)
    fxmin, fymin, fzmin, fxmax, fymax, fzmax = first.bounding_box()
    sxmin, symin, szmin, sxmax, symax, szmax = second.bounding_box()
    # Minimums closer than epsilon are ties, so numerical noise cannot decide the order.
    if not near_eq(fxmin, sxmin):
        first_is_first = fxmin < sxmin
    elif not near_eq(fzmin, szmin):
        first_is_first = fzmin < szmin
    else:
        first_is_first = first.volume() <= second.volume() + Const.epsilon
    if first_is_first:
        return Obj3d(first), Obj3d(second)
    else:
        return Obj3d(second), Obj3d(first)

This works similarly to a miter saw. It cuts this object into two parts using a plane defined by the cut_angle and cut_point. Cut_angle is in degrees, and cut_point is a point through which the cut plane passes. The cut is parallel to the Y axis, with the angle defining the slope of the cut. The two parts are returned as a tuple of Obj3d objects. The returned order is the one with the least x, or then the least z, or then the least volume.

def num_faces(self) ‑> int
Expand source code
def num_faces(self) -> int:
    """
    The number of faces in this object.

    This is useful in unit testing shapes.

    It is very difficult to check that a shape is correct, but knowing that a correct
    number of faces is present goes a long way to making sure things are healthy.
    """
    return self.mo.num_tri()

The number of faces in this object.

This is useful in unit testing shapes.

It is very difficult to check that a shape is correct, but knowing that a correct number of faces is present goes a long way to making sure things are healthy.

def num_verts(self) ‑> int
Expand source code
def num_verts(self) -> int:
    """
    The number of vertices in this object.

    This is useful in unit testing shapes.

    It is very difficult to check that a shape is correct, but knowing that a correct
    number of vertices is present goes a long way to making sure things are healthy.
    """
    return self.mo.num_vert()

The number of vertices in this object.

This is useful in unit testing shapes.

It is very difficult to check that a shape is correct, but knowing that a correct number of vertices is present goes a long way to making sure things are healthy.

def piecut(self, start_angle=0, end_angle=90, both=False) ‑> Obj3d | tuple[Obj3d, Obj3d]
Expand source code
def piecut(
    self, start_angle=0, end_angle=90, both=False
) -> Obj3d | tuple[Obj3d, Obj3d]:
    """
    Cut a wedge out of this object.

    It returns a copy of this object minus the wedge.

    Or if `both` is `True` it returns a tuple of the object minus the wedge and the object intersected with the wedge.

    """
    if end_angle < start_angle:
        end_angle = end_angle + 360.0
    if end_angle - start_angle >= 360.0:
        raise ValidationError(
            "Parameters start_angle and end_angle must be less than 360 degrees apart."
        )
    x1, y1, z1, x2, y2, z2 = self.bounding_box()
    c_x = (x2 + x1) / 2.0
    c_y = (y2 + y1) / 2.0
    c_z = (z2 + z1) / 2.0
    rad = max(z2 - z1, x2 - x1, y2 - y1) * 4
    h = z2 - z1
    pts = []
    if end_angle - start_angle != 180.0:
        pts.append((c_x, c_y))
    pts.append((rad * cos(start_angle) + c_x, rad * sin(start_angle) + c_y))
    ang = 90 + start_angle
    while ang < end_angle:
        pts.append((rad * cos(ang) + c_x, rad * sin(ang) + c_y))
        ang = ang + 90
    pts.append((rad * cos(end_angle) + c_x, rad * sin(end_angle) + c_y))
    cutter = Obj3d(_m.Manifold.extrude(_m.CrossSection([pts]), h)).translate(
        (0, 0, z1)
    )
    if both:
        o2, o1 = self.split(cutter)
        return (o1, o2)
    o3 = difference(self, cutter)
    return o3

Cut a wedge out of this object.

It returns a copy of this object minus the wedge.

Or if both is True it returns a tuple of the object minus the wedge and the object intersected with the wedge.

def project(self) ‑> Obj2d
Expand source code
def project(self) -> Obj2d:
    """
    Return a Obj2d representing this object's "shadow" on the x-y plane.

    """
    return Obj2d(self.mo.project())

Return a Obj2d representing this object's "shadow" on the x-y plane.

def resize(self, sizes: list[tuple[float | None, float | None, float | None]]) ‑> Obj3d
Expand source code
def resize(
    self, sizes: list[tuple[float | None, float | None, float | None]]
) -> Obj3d:
    """
    Resize this object to a specific size on each axis.

    If an axis size is specified as zero, it is automatically calculated by a specified size to
    maintain the same size ratio between the two sizes.

    If an axis size is specified as `None`, no change is made to that axis.
    """
    _chkV3("sizes", sizes)
    if (
        (sizes[0] == None or sizes[0] == 0)
        and (sizes[1] == None or sizes[1] == 0)
        and (sizes[2] == None or sizes[2] == 0)
    ):
        return self.scale((1, 1, 1))

    min_x, min_y, min_z, max_x, max_y, max_z = self.bounding_box()
    self_sizes = [max_x - min_x, max_y - min_y, max_z - min_z]

    auto = False
    for i in range(len(sizes)):
        if sizes[i] == 0:
            auto = True
            break
    sf = [1, 1, 1]
    scale = 1
    scaled_cnt = 0
    for i in range(len(sizes)):
        if sizes[i] == None:
            continue
        if sizes[i] == 0:
            continue
        scale = sf[i] = sizes[i] / self_sizes[i]
        scaled_cnt = scaled_cnt + 1
    if auto and scaled_cnt != 1:
        raise ValidationError(
            "If you're using auto, then only one size can be specified."
        )
    for i in range(len(sizes)):
        if sizes[i] == 0:
            sf[i] = scale
    return self.scale(sf)

Resize this object to a specific size on each axis.

If an axis size is specified as zero, it is automatically calculated by a specified size to maintain the same size ratio between the two sizes.

If an axis size is specified as None, no change is made to that axis.

def rotate(self, degrees: list[float, float, float]) ‑> Obj3d
Expand source code
def rotate(self, degrees: list[float, float, float]) -> Obj3d:
    """
    Rotate this object by the given degrees for each axis.

    """
    _chkV3("degrees", degrees)
    return Obj3d(self.mo.rotate(degrees))

Rotate this object by the given degrees for each axis.

def scale(self, factors: list[float, float, float]) ‑> Obj3d
Expand source code
def scale(self, factors: list[float, float, float]) -> Obj3d:
    """
    Scale this object by the given factors.

    If you want no change, use `1.0`, that means 100% (thus unchanged).
    """
    _chkV3("factors", factors)
    return Obj3d(self.mo.scale(factors))

Scale this object by the given factors.

If you want no change, use 1.0, that means 100% (thus unchanged).

def simplify(self, tolerance: float = 0) ‑> Obj3d
Expand source code
def simplify(self, tolerance: float = 0) -> Obj3d:
    """
    Return a simplified version of this object.

    Using a tolerance of 0 means that the current tolerance will be used.
    """
    _chkGE("tolerance", tolerance, 0)
    return Obj3d(self.mo.simplify(tolerance))

Return a simplified version of this object.

Using a tolerance of 0 means that the current tolerance will be used.

def slice(self, height: float) ‑> Obj2d
Expand source code
def slice(self, height: float) -> Obj2d:
    """
    Like `project`, but rather than the bottom, project at the given height.

    """
    _chkGT("height", height, 0)
    return Obj2d(self.mo.slice(height))

Like project, but rather than the bottom, project at the given height.

def split(self, cutter: Obj3d) ‑> Obj3d
Expand source code
def split(self, cutter: Obj3d) -> Obj3d:
    """
    This performs difference and intersection operations between this object and the cutter more efficiently.

    The return value is `(inter_obj, diff_obj)`.

    """
    ret = self.mo.split(cutter.mo)
    return (Obj3d(ret[0]), Obj3d(ret[1]))

This performs difference and intersection operations between this object and the cutter more efficiently.

The return value is (inter_obj, diff_obj).

def surface_area(self) ‑> float
Expand source code
def surface_area(self) -> float:
    """
    The surface area of this Obj3d.
    """
    return self.mo.surface_area()

The surface area of this Obj3d.

def to_verts_and_faces(self, np: bool = False) ‑> tuple[list[tuple[float, float, float]], list[tuple[int, int, int]]]
Expand source code
def to_verts_and_faces(
    self, np: bool = False
) -> tuple[list[tuple[float, float, float]], list[tuple[int, int, int]]]:
    """
    Return a pair containing a list of vertices and a list of faces for this object.

    Manifold3d uses numpy arrays to avoid an array copy. For most uses, python lists of tuples are
    more efficient. However, if you are using numpy for further processing, you may want to set `np=True`,
    to preserve the arrays as numpy arrays.
    Numpy is faster at vectorizable operations, but is often 10x slower for smaller operations such as `cross` and `dot`,
    so you may want to do timing tests.

    """
    mesh = self.mo.to_mesh64()
    if mesh.vert_properties.shape[1] > 3:
        vertices = mesh.vert_properties[:, :3]
    else:
        vertices = mesh.vert_properties
    if np:
        return (vertices, mesh.tri_verts)
    v = list(map(tuple, vertices.tolist()))
    f = list(map(tuple, mesh.tri_verts.tolist()))
    return (v, f)

Return a pair containing a list of vertices and a list of faces for this object.

Manifold3d uses numpy arrays to avoid an array copy. For most uses, python lists of tuples are more efficient. However, if you are using numpy for further processing, you may want to set np=True, to preserve the arrays as numpy arrays. Numpy is faster at vectorizable operations, but is often 10x slower for smaller operations such as cross and dot, so you may want to do timing tests.

def transform(self,
matrix3x4: tuple[tuple[float, float, float, float], tuple[float, float, float, float], tuple[float, float, float, float], tuple[float, float, float, float]]) ‑> Obj3d
Expand source code
def transform(
    self,
    matrix3x4: tuple[
        tuple[float, float, float, float],
        tuple[float, float, float, float],
        tuple[float, float, float, float],
        tuple[float, float, float, float],
    ],
) -> Obj3d:
    """
    Transform this object with the given affine transformation matrix.

    If you don't know what this is, you probably don't need it.
    """
    if (
        len(matrix3x4) != 3
        or len(matrix3x4[0]) != 4
        or len(matrix3x4[1]) != 4
        or len(matrix3x4[2]) != 4
    ):
        raise ValueError("Improperly sized 3x4 matrix.")

    return Obj3d(self.mo.transform(matrix3x4))

Transform this object with the given affine transformation matrix.

If you don't know what this is, you probably don't need it.

def translate(self, offsets: list[float, float, float]) ‑> Obj3d
Expand source code
def translate(self, offsets: list[float, float, float]) -> Obj3d:
    """
    Translate (move) this object by the given offsets.
    """
    _chkV3("offsets", offsets)
    return Obj3d(self.mo.translate(offsets))

Translate (move) this object by the given offsets.

def volume(self) ‑> float
Expand source code
def volume(self) -> float:
    """
    The volume of this Obj3d.
    """
    return self.mo.volume()

The volume of this Obj3d.

class ValidationError (message)
Expand source code
class ValidationError(Exception):
    """
    Exception class for errors detected in arguments to **piecad**
    functions and methods.
    """

    def __init__(self, message):
        super().__init__(message)

Exception class for errors detected in arguments to piecad functions and methods.

Ancestors

  • builtins.Exception
  • builtins.BaseException