Module piecad.primitives_3d

Create 3D objects such as spheres and cubes.

Functions

def chamfer(obj: Obj3d,
bevel_size: float = 2.0,
min_edge_length: float = 2.0,
angle_range: tuple[float, float] = (80, 100),
include: list[tuple[float, float, float, float, float, float]] = None,
exclude: list[tuple[float, float, float, float, float, float]] = None) ‑> Obj3d
Expand source code
def chamfer(
    obj: Obj3d,
    bevel_size: float = 2.0,
    min_edge_length: float = 2.0,
    angle_range: tuple[float, float] = (80, 100),
    include: list[tuple[float, float, float, float, float, float]] = None,
    exclude: list[tuple[float, float, float, float, float, float]] = None,
) -> Obj3d:
    """
    EXPERIMENTAL - interface may change in future releases.
    Chamfer the outward-facing edges of an `Obj3d` object.

    The `bevel_size` parameter controls the width of the chamfer applied to the edges of the object.

    The `min_edge_length` parameter specifies the minimum length of edges to consider for chamfering.
    This is useful in avoiding the chamfering of circular objects. Circular edges often chamfer poorly.

    The `angle_range` parameter specifies the range of angles (in degrees) for edges to be
    considered for chamfering. (The angle is between the two faces adjacent to the edge.)
    The further one gets from 90 degrees, the more likely that difficulties may arise during
    the chamfering process.

    The `include` and `exclude` parameters allow you to specify which edges should be chamfered or ignored.
    By default, all outward-facing edges of the object will be chamfered.
    Both the `include` and `exclude` lists are "bounding boxes" that define the regions of
    space in which edges will be considered for chamfering or ignored.
    Each bounding box is a sextuple of the form `(xmin, ymin, zmin, xmax, ymax, zmax)`.
    Only edges that have both endpoints within the bounding boxes are selected.
    If the include list is provided, only the edges selected in the list will be chamfered.
    If the exclude list is provided, the edges selected in the list will be ignored.
    The exclude list always takes precedence over the include list.

    If you want a similar size between fillets and chamfers, choose a bevel_size
    for the chamfer that is approximately half the size of the radius you would use for a fillet.
    """
    from ._f_and_c import do_f_and_c

    return do_f_and_c(
        obj,
        fillet=False,
        min_edge_length=min_edge_length,
        angle_range=angle_range,
        radius=bevel_size,
        include=include,
        exclude=exclude,
    )

EXPERIMENTAL - interface may change in future releases. Chamfer the outward-facing edges of an Obj3d object.

The bevel_size parameter controls the width of the chamfer applied to the edges of the object.

The min_edge_length parameter specifies the minimum length of edges to consider for chamfering. This is useful in avoiding the chamfering of circular objects. Circular edges often chamfer poorly.

The angle_range parameter specifies the range of angles (in degrees) for edges to be considered for chamfering. (The angle is between the two faces adjacent to the edge.) The further one gets from 90 degrees, the more likely that difficulties may arise during the chamfering process.

The include and exclude parameters allow you to specify which edges should be chamfered or ignored. By default, all outward-facing edges of the object will be chamfered. Both the include and exclude lists are "bounding boxes" that define the regions of space in which edges will be considered for chamfering or ignored. Each bounding box is a sextuple of the form (xmin, ymin, zmin, xmax, ymax, zmax). Only edges that have both endpoints within the bounding boxes are selected. If the include list is provided, only the edges selected in the list will be chamfered. If the exclude list is provided, the edges selected in the list will be ignored. The exclude list always takes precedence over the include list.

If you want a similar size between fillets and chamfers, choose a bevel_size for the chamfer that is approximately half the size of the radius you would use for a fillet.

def cone(height: float,
radius_low: float,
radius_high: float = 0.2,
segments: int = -1,
center: bool = False) ‑> Obj3d
Expand source code
def cone(
    height: float,
    radius_low: float,
    radius_high: float = 0.2,
    segments: int = -1,
    center: bool = False,
) -> Obj3d:
    """
    Make a cone with given radii and height.

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

    Why does `radius_high` not default to 0? Pointy things
    don't 3d print very well. If the model is not to be printed,
    by all means set it to 0.

    By default, the cone bottom is centered at `(0,0,0)`.
    When `center` is `True`, the cone will be centered on `(0,0,0)`.
    (In other words, the bottom of the cone will be at `(0,0,-height/2.0`.)

    <iframe width="100%" height="220" src="examples/cone.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGT("height", height, 0)
    _chkGT("radius_low", radius_low, 0)
    _chkGE("radius_high", radius_high, 0)
    _chkGE("segments", segments, 3)
    return Obj3d(
        _m.Manifold.cylinder(height, radius_low, radius_high, segments, center)
    )

Make a cone with given radii and height.

For segments see the documentation of Config.set_default_segments.

Why does radius_high not default to 0? Pointy things don't 3d print very well. If the model is not to be printed, by all means set it to 0.

By default, the cone bottom is centered at (0,0,0). When center is True, the cone will be centered on (0,0,0). (In other words, the bottom of the cone will be at (0,0,-height/2.0.)

def cube(size: float, center: bool = False) ‑> Obj3d
Expand source code
def cube(size: float, center: bool = False) -> Obj3d:
    """
    Make a cube of with sides of the given size.

    By default, the bottom front left corner of the cube will be at `(0,0,0)`.
    When `center` is `True` it will cause the cube to be centered at `(0,0,0)`.

    """
    # Too many models on one page. <iframe width="100%" height="220" src="examples/cube.html"></iframe>
    if type(size) == list or type(size) == tuple:
        return cuboid(size, center)
    return Obj3d(_m.Manifold.cube((size, size, size), center))

Make a cube of with sides of the given size.

By default, the bottom front left corner of the cube will be at (0,0,0). When center is True it will cause the cube to be centered at (0,0,0).

def cuboid(size: list[float, float, float], center: bool = False) ‑> Obj3d
Expand source code
def cuboid(size: list[float, float, float], center: bool = False) -> Obj3d:
    """
    Make a cuboid with the x, y, and z values given in size.

    By default, the bottom front left corner of the cuboid will be at `(0,0,0)`.
    When `center` is `True` it will cause the cube to be centered at `(0,0,0)`.

    """
    # Too many models on one page. <iframe width="100%" height="220" src="examples/cuboid.html"></iframe>
    if type(size) == float or type(size) == int:
        return cube(size)
    return Obj3d(_m.Manifold.cube(size, center))

Make a cuboid with the x, y, and z values given in size.

By default, the bottom front left corner of the cuboid will be at (0,0,0). When center is True it will cause the cube to be centered at (0,0,0).

def cylinder(height: float, radius: float, segments: int = -1, center=False) ‑> Obj3d
Expand source code
def cylinder(height: float, radius: float, segments: int = -1, center=False) -> Obj3d:
    """
    Make a cylinder of a given radius and height.

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

    By default, the cylinder bottom is centered at `(0,0,0)`.
    When `center` is `True`, the cylinder will centered on `(0,0,0)`.
    (In other words, the bottom of the cylinder will be at `(0,0,-height/2.0`.)
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGT("height", height, 0)
    _chkGT("radius", radius, 0)
    _chkGE("segments", segments, 3)
    cyl = circle(radius, segments).extrude(height)
    if center:
        cyl = cyl.translate((0, 0, -height / 2.0))
    return cyl

Make a cylinder of a given radius and height.

For segments see the documentation of Config.set_default_segments.

By default, the cylinder bottom is centered at (0,0,0). When center is True, the cylinder will centered on (0,0,0). (In other words, the bottom of the cylinder will be at (0,0,-height/2.0.)

def ellipsoid(radii: tuple[float, float, float], segments: int = -1, center=False) ‑> Obj3d
Expand source code
def ellipsoid(
    radii: tuple[float, float, float], segments: int = -1, center=False
) -> Obj3d:
    """
    Make an ellipsoid which is an elliptical on all three radii.

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

    The ellipsoid is centered at `(0,0,0)`.

    <iframe width="100%" height="220" src="examples/ellipsoid.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkV3("radius", radii)
    _chkGE("segments", segments, 3)

    return sphere(1, segments=segments).scale(radii)

Make an ellipsoid which is an elliptical on all three radii.

For segments see the documentation of Config.set_default_segments.

The ellipsoid is centered at (0,0,0).

def elliptical_cylinder(height: float, radii: tuple[float, float], segments: int = -1, center=False) ‑> Obj3d
Expand source code
def elliptical_cylinder(
    height: float, radii: tuple[float, float], segments: int = -1, center=False
) -> Obj3d:
    """
    Make a elliptically shaped cylinder of given radii and height.

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

    By default, the elliptical cylinder bottom is centered at `(0,0,0)`.
    When `center` is `True`, the cylinder will centered on `(0,0,0)`.
    (In other words, the bottom of the elliptical cylinder will be at `(0,0,-height/2.0`.)

    <iframe width="100%" height="220" src="examples/elliptical_cylinder.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGT("height", height, 0)
    _chkV2("radii", radii)
    _chkGE("segments", segments, 3)

    ecyl = extrude(ellipse(radii, segments), height)
    if center:
        ecyl = ecyl.translate((0, 0, -height / 2.0))
    return ecyl

Make a elliptically shaped cylinder of given radii and height.

For segments see the documentation of Config.set_default_segments.

By default, the elliptical cylinder bottom is centered at (0,0,0). When center is True, the cylinder will centered on (0,0,0). (In other words, the bottom of the elliptical cylinder will be at (0,0,-height/2.0.)

def extrude(obj: Obj2d,
height: float) ‑> Obj3d
Expand source code
def extrude(obj: Obj2d, height: float) -> Obj3d:
    """
    Create a Obj3d solid from Obj2d of given height.

    The 2d object will be copied and moved up to `height`.
    Lines will be added creating an `obj`-shaped 3d solid.

    <iframe width="100%" height="220" src="examples/extrude.html"></iframe>
    """
    _chkTY("obj", obj, Obj2d)
    _chkGT("height", height, 0)
    return Obj3d(_m.Manifold.extrude(obj.mo, height))

Create a Obj3d solid from Obj2d of given height.

The 2d object will be copied and moved up to height. Lines will be added creating an obj-shaped 3d solid.

def extrude_chaining(pairs: list[tuple[float, Obj2d]],
is_convex: bool = False,
diagnose: str = None) ‑> Obj3d
Expand source code
def extrude_chaining(
    pairs: list[tuple[float, Obj2d]], is_convex: bool = False, diagnose: str = None
) -> Obj3d:
    """
    Extrude multiple 2d objects into a single 3d Object.

    ALL 2D OBJECTS MUST HAVE THE SAME NUMBER OF POINTS.

    Parameter `pairs` is a list of pairs: `[[height, Obj2d], ...]`.
    This list controls an extrusion of 2d shapes chained together
    into one 3d object.

    The `height` is cumulative. You are always specifing the exact current height
    to be output for the current object. This is done so that you can have numerically
    robust dimensions in your object. If relative heights were used, extuding something
    like a sphere is would end up with a sphere that's height was not precisely
    the desired height.

    If parameter `is_convex` is set to `True` a much faster triangulation
    algoritm is used, which only works on a convex shape. If you don't
    understand what this means, leave this option alone.

    Caps are automatically generated from the first and last shapes.

    If `diagnose` is not `None`, it should contain a string of a filename where the
    faulty object is stored (to look at with meshlab, for example).
    Use of the `.obj` extension is recommended.

    <iframe width="100%" height="550" src="examples/extrude_chaining.html"></iframe>
    """
    vertex_list = []
    triangles = []

    _chkGT("pairs length", len(pairs), 1)

    def add_cap(v_off, polys, top):

        if not is_convex or len(polys) != 1:
            tris = _m.triangulate(polys)
            for t in tris:
                if top:
                    triangles.append((t[0] + v_off, t[1] + v_off, t[2] + v_off))
                else:  # Bottom caps are reversed.
                    triangles.append((t[2] + v_off, t[1] + v_off, t[0] + v_off))
        else:  # Fan triangulation
            n = len(poly)  # We have only one to deal with
            chosen = 0
            for i in range(1, n - 1):
                cur = i
                next = i + 1
                if top:
                    triangles.append((chosen + v_off, cur + v_off, next + v_off))
                else:  # Bottom caps are clockwise.
                    triangles.append((next + v_off, cur + v_off, chosen + v_off))

    v_offs = []
    v_offs.append(0)
    o2d_polys = []
    for h, o2d in pairs:
        if o2d.is_empty():
            raise ValidationError(
                f"At pairs index: {cur_idx}, empty shape is not allowed"
            )
        polys = o2d.to_paths()
        o2d_polys.append(polys)
        for poly in polys:
            for vert in poly:
                vertex_list.append((vert[0], vert[1], h))
        v_offs.append(len(vertex_list))
    v_offs.pop()

    prev_polys = o2d_polys[0]
    prev_vo = 0
    add_cap(prev_vo, prev_polys, top=False)
    last = len(pairs)
    cur_idx = 1

    while cur_idx < last:
        cur_polys = o2d_polys[cur_idx]
        cur_vo = v_offs[cur_idx]

        if len(cur_polys) != len(prev_polys):
            raise ValidationError(
                f"At pairs index: {cur_idx}, previous shape does not match current shape"
            )

        for i in range(0, len(cur_polys)):
            b = prev_polys[i]
            t = cur_polys[i]

            if len(b) != len(t):
                raise ValidationError(
                    f"At pairs index: {cur_idx}, poly: {i}, previous shape does not match current shape"
                )

            bottom_p = 0 + prev_vo
            top_p = 0 + cur_vo
            _len = len(b)

            for j in range(0, _len):
                next_bottom_p = ((j + 1) % _len) + prev_vo
                next_top_p = ((j + 1) % _len) + cur_vo
                triangles.append((bottom_p, next_bottom_p, next_top_p))
                triangles.append((bottom_p, next_top_p, top_p))
                bottom_p = next_bottom_p
                top_p = next_top_p

            prev_vo = cur_vo
            cur_vo += _len

        prev_polys = cur_polys
        prev_vo = v_offs[cur_idx]
        cur_idx += 1

    add_cap(prev_vo, prev_polys, top=True)

    if diagnose != None:
        dot_idx = diagnose.rindex(".")
        ext = diagnose[dot_idx + 1 :]
        mesh_output = trimesh.Trimesh(vertices=vertex_list, faces=triangles)
        trimesh.exchange.export.export_mesh(mesh_output, diagnose, ext)
    return obj3d_from_vertices_and_faces(vertex_list, triangles)

Extrude multiple 2d objects into a single 3d Object.

ALL 2D OBJECTS MUST HAVE THE SAME NUMBER OF POINTS.

Parameter pairs is a list of pairs: [[height, Obj2d], …]. This list controls an extrusion of 2d shapes chained together into one 3d object.

The height is cumulative. You are always specifing the exact current height to be output for the current object. This is done so that you can have numerically robust dimensions in your object. If relative heights were used, extuding something like a sphere is would end up with a sphere that's height was not precisely the desired height.

If parameter is_convex is set to True a much faster triangulation algoritm is used, which only works on a convex shape. If you don't understand what this means, leave this option alone.

Caps are automatically generated from the first and last shapes.

If diagnose is not None, it should contain a string of a filename where the faulty object is stored (to look at with meshlab, for example). Use of the .obj extension is recommended.

def extrude_transforming(obj: Obj2d,
height: float,
num_twist_divisions: int = 0,
twist: float = 0,
scale: tuple[float, float] = (1.0, 1.0)) ‑> Obj3d
Expand source code
def extrude_transforming(
    obj: Obj2d,
    height: float,
    num_twist_divisions: int = 0,
    twist: float = 0,
    scale: tuple[float, float] = (1.0, 1.0),
) -> Obj3d:
    """
    Create a Obj3d solid from Obj2d of given height.

    The 2d object will be copied and moved up to `height`.
    Lines will be added creating an `obj`-shaped 3d solid.

    Parameter `num_twist_divisions` should only be used when `twist` is
    greater than zero.

    Parameter `twist` will cause a circular rotation for each `num_twist_divisions`.

    Parammeter `scale` specifes scaling factors applied at each division.

    <iframe width="100%" height="300" src="examples/extrude_transforming.html"></iframe>
    """
    _chkTY("obj", obj, Obj2d)
    _chkGT("height", height, 0)
    _chkGE("num_twist_divisions", num_twist_divisions, 0)
    _chkGE("twist", twist, 0)
    _chkV2("scale", scale)
    return Obj3d(
        _m.Manifold.extrude(
            obj.mo,
            height,
            num_twist_divisions,
            twist,
            scale,
        )
    )

Create a Obj3d solid from Obj2d of given height.

The 2d object will be copied and moved up to height. Lines will be added creating an obj-shaped 3d solid.

Parameter num_twist_divisions should only be used when twist is greater than zero.

Parameter twist will cause a circular rotation for each num_twist_divisions.

Parammeter scale specifes scaling factors applied at each division.

def fillet(obj: Obj3d,
radius: float = 4.0,
min_edge_length: float = 2.0,
angle_range: tuple[float, float] = (80, 100),
include: list[tuple[float, float, float, float, float, float]] = None,
exclude: list[tuple[float, float, float, float, float, float]] = None) ‑> Obj3d
Expand source code
def fillet(
    obj: Obj3d,
    radius: float = 4.0,
    min_edge_length: float = 2.0,
    angle_range: tuple[float, float] = (80, 100),
    include: list[tuple[float, float, float, float, float, float]] = None,
    exclude: list[tuple[float, float, float, float, float, float]] = None,
) -> Obj3d:
    """
    EXPERIMENTAL - interface may change in future releases.
    Fillet the edges of an `Obj3d` object.

    The `radius` parameter specifies the  radius of the quarter cylinder that defines the fillet.

    The `min_edge_length` parameter specifies the minimum length of edges to consider for filleting.
    This is useful in avoiding the filleting of circular objects. Circular edges often fillet poorly.

    The `angle_range` parameter specifies the range of angles (in degrees) for edges to be
    considered for filleting. (The angle is between the two faces adjacent to the edge.)
    The further one gets from 90 degrees, the more likely that difficulties may arise during
    the filleting process.

    The `include` and `exclude` parameters allow you to specify which edges should be filleted or ignored.
    By default, all outward-facing edges of the object will be filleted.
    Both the `include` and `exclude` lists are "bounding boxes" that define the regions of
    space in which edges will be considered for filleting or ignored.
    Each bounding box is a sextuple of the form `(xmin, ymin, zmin, xmax, ymax, zmax)`.
    Only edges that have both endpoints within the bounding boxes are selected.
    If the include list is provided, only the edges selected in the list will be filleted.
    If the exclude list is provided, the edges selected in the list will be ignored.
    The exclude list always takes precedence over the include list.

    If you want a similar size between fillets and chamfers, choose a bevel_size
    for the chamfer that is approximately half the size of the radius you would use for a fillet.
    """
    from ._f_and_c import do_f_and_c

    return do_f_and_c(
        obj,
        fillet=True,
        radius=radius,
        min_edge_length=min_edge_length,
        angle_range=angle_range,
        include=include,
        exclude=exclude,
    )

EXPERIMENTAL - interface may change in future releases. Fillet the edges of an Obj3d object.

The radius parameter specifies the radius of the quarter cylinder that defines the fillet.

The min_edge_length parameter specifies the minimum length of edges to consider for filleting. This is useful in avoiding the filleting of circular objects. Circular edges often fillet poorly.

The angle_range parameter specifies the range of angles (in degrees) for edges to be considered for filleting. (The angle is between the two faces adjacent to the edge.) The further one gets from 90 degrees, the more likely that difficulties may arise during the filleting process.

The include and exclude parameters allow you to specify which edges should be filleted or ignored. By default, all outward-facing edges of the object will be filleted. Both the include and exclude lists are "bounding boxes" that define the regions of space in which edges will be considered for filleting or ignored. Each bounding box is a sextuple of the form (xmin, ymin, zmin, xmax, ymax, zmax). Only edges that have both endpoints within the bounding boxes are selected. If the include list is provided, only the edges selected in the list will be filleted. If the exclude list is provided, the edges selected in the list will be ignored. The exclude list always takes precedence over the include list.

If you want a similar size between fillets and chamfers, choose a bevel_size for the chamfer that is approximately half the size of the radius you would use for a fillet.

def geodesic_sphere(radius, segments=-1) ‑> Obj3d
Expand source code
def geodesic_sphere(radius, segments=-1) -> Obj3d:
    """
    Create a geodesic sphere of a given radius.

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

    <iframe width="100%" height="220" src="examples/geodesic_sphere.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGT("radius", radius, 0)
    _chkGE("segments", segments, 3)

    return Obj3d(_m.Manifold.sphere(radius, segments))

Create a geodesic sphere of a given radius.

For segments see the documentation of Config.set_default_segments.

def lithophane(image_filename: str,
width_mm: int = 150,
pixel_size: float = 0.5,
min_thickness: float = 0.8,
max_thickness: float = 3.0) ‑> Obj3d
Expand source code
def lithophane(
    image_filename: str,
    width_mm: int = 150,
    pixel_size: float = 0.5,
    min_thickness: float = 0.8,
    max_thickness: float = 3.0,
) -> Obj3d:
    """
    Create a 3d lithophane from a 2d image.

    Specify the desired size in millimeters by setting `width_mm`.
    Height will be calculated by using the image's aspect ratio.

    Set resolution with `pixel_size`, which specifies the number of pixels per millimeter.

    The range of lithophane thickness in millimeters is specified by `min_thickness` and `max_thickness`.
    The range is usually good for white PLA filament. If using a clear filament you will need to experiment
    to find the best range.
    """

    hm = _lithophane.load_heightmap(image_filename, width_mm / pixel_size)

    o = _lithophane.create_lithophane(hm, pixel_size, min_thickness, max_thickness)

    return o

Create a 3d lithophane from a 2d image.

Specify the desired size in millimeters by setting width_mm. Height will be calculated by using the image's aspect ratio.

Set resolution with pixel_size, which specifies the number of pixels per millimeter.

The range of lithophane thickness in millimeters is specified by min_thickness and max_thickness. The range is usually good for white PLA filament. If using a clear filament you will need to experiment to find the best range.

def polyhedron(vertices: list[tuple[float, float, float]],
faces: list[tuple[int, int, int]],
check: str = 'interactive') ‑> Obj3d
Expand source code
def polyhedron(
    vertices: list[tuple[float, float, float]],
    faces: list[tuple[int, int, int]],
    check: str = "interactive",
) -> Obj3d:
    """
    Create an Obj3d from points and a list of triangles using those points.

    THIS IS AN ADVANCED FUNCTION.

    If you don't already understand the "directions" below... REALLY, try doing
    what you want to do another way.

    * Faces have to be wound counter-clockwise.

    * All faces must be triangles.

    * Faces are integer indices into the vertices list.

    You eventually will get a message from the Manifold package saying it is not "manifold".

    That isn't really helpful.

    Thus, we added our own checking.
    By default `check` is interactive. If your polyhedron is faulty it will start
    an interactive program to help you understand what is wrong.
    If  `check` is `"batch"` a `ValidationError` is thrown if the polyhedron is bad.
    The `ValidationError` will have short message saying what is wrong.
    If you set `check` to `"none"` no checking is performed... you're on your own!

    If all else fails, try adding these lines just before the call to polyhedron.

    ```python
    mesh_output = trimesh.Trimesh(vertices=vertices, faces=triangles)
    trimesh.exchange.export.export_mesh(mesh_output, "mesh.obj", "obj")
    ```

    Then use a program like `meshlab` to look at where things are not manifold.

    """
    _chkIn("check", check, ["interactive", "batch", "repair", "none"])
    o = obj3d_from_vertices_and_faces(vertices, faces, check)
    return o

Create an Obj3d from points and a list of triangles using those points.

THIS IS AN ADVANCED FUNCTION.

If you don't already understand the "directions" below… REALLY, try doing what you want to do another way.

  • Faces have to be wound counter-clockwise.

  • All faces must be triangles.

  • Faces are integer indices into the vertices list.

You eventually will get a message from the Manifold package saying it is not "manifold".

That isn't really helpful.

Thus, we added our own checking. By default check is interactive. If your polyhedron is faulty it will start an interactive program to help you understand what is wrong. If check is "batch" a ValidationError is thrown if the polyhedron is bad. The ValidationError will have short message saying what is wrong. If you set check to "none" no checking is performed… you're on your own!

If all else fails, try adding these lines just before the call to polyhedron.

mesh_output = trimesh.Trimesh(vertices=vertices, faces=triangles)
trimesh.exchange.export.export_mesh(mesh_output, "mesh.obj", "obj")

Then use a program like meshlab to look at where things are not manifold.

def pyramid(height: int, num_sides: int, radius: float) ‑> Obj3d
Expand source code
def pyramid(height: int, num_sides: int, radius: float) -> Obj3d:
    """
    Make a regular pyramid with the given height and number of sides.

    The `radius` specifies the circle on which the corners of the pyramid will be built.

    <iframe width="100%" height="220" src="examples/pyramid.html"></iframe>
    """
    _chkGT("height", height, 0)
    _chkGE("num_sides", num_sides, 3)
    _chkGT("radius", radius, 0)
    return extrude_transforming(
        circle(radius, segments=num_sides), height=height, scale=(0, 0)
    )

Make a regular pyramid with the given height and number of sides.

The radius specifies the circle on which the corners of the pyramid will be built.

def revolve(obj: Obj2d,
revolve_degrees: float = 360.0,
segments: int = -1) ‑> Obj3d
Expand source code
def revolve(obj: Obj2d, revolve_degrees: float = 360.0, segments: int = -1) -> Obj3d:
    """
    Create a Obj3d by revolving an Obj2d 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).

    <iframe width="100%" height="220" src="examples/revolve.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkTY("obj", obj, Obj2d)
    _chkGE("segments", segments, 3)
    _chkGT("revolve_degrees", revolve_degrees, 0)
    return Obj3d(_m.Manifold.revolve(obj.mo, segments, revolve_degrees))

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

For segments see the documentation of Config.set_default_segments.

def rounded_cuboid(size: list[float, float, float],
rounding_radius=2.0,
segments: int = -1,
center: bool = False) ‑> Obj3d
Expand source code
def rounded_cuboid(
    size: list[float, float, float],
    rounding_radius=2.0,
    segments: int = -1,
    center: bool = False,
) -> Obj3d:
    """
    Make a rounded_cuboid with the x, y, and z values given in size.

    Parameter `rounding_radius` is the size of the rounded lip at top and bottom.

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

    By default, the bottom front left corner of the rounded cuboid will be at `(0,0,0)`.
    When `center` is `True` it will cause the rounded cuboid to be centered at `(0,0,0)`.

    <iframe width="100%" height="250" src="examples/rounded_cuboid.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGE("segments", segments, 3)
    _chkGE("rounding_radius", rounding_radius, 0)
    _chkV3("size", size)
    if type(size) == float or type(size) == int:
        size = (size, size, size)
    l = []
    res = Config.get_layer_resolution()
    arc_segs = rounding_radius / res
    deg_per_arc_seg = 90.0 / arc_segs
    deg = 0.0
    cur_z = 0
    x, y, z = size
    rr = rounding_radius
    ix = x - 2 * rr
    iy = y - 2 * rr
    smallest_rr = sin(deg_per_arc_seg) / 2.0
    l.append(
        (cur_z, rounded_rectangle((ix, iy), smallest_rr, segments).translate((rr, rr)))
    )
    deg += deg_per_arc_seg
    while deg < 90.0:
        delta = rr * sin(deg)
        cur_z = rr - rr * cos(deg)
        l.append(
            (
                cur_z,
                rounded_rectangle(
                    (ix + 2 * delta, iy + 2 * delta), delta, segments
                ).translate((rr - delta, rr - delta)),
            )
        )
        deg += deg_per_arc_seg

    cur_z = rr
    l.append((cur_z, rounded_rectangle((x, y), rr, segments)))

    cur_z = z - rr
    l.append((cur_z, rounded_rectangle((x, y), rr, segments)))

    deg = 90.0
    deg -= deg_per_arc_seg
    while deg > 0.0:
        delta = rr * sin(deg)
        cur_z = z - rr + rr * cos(deg)
        l.append(
            (
                cur_z,
                rounded_rectangle(
                    ((ix) + 2 * delta, (iy) + 2 * delta), delta, segments
                ).translate((rr - delta, rr - delta)),
            )
        )
        deg -= deg_per_arc_seg

    cur_z = z
    l.append(
        (cur_z, rounded_rectangle((ix, iy), smallest_rr, segments).translate((rr, rr)))
    )

    o = extrude_chaining(l, is_convex=True)
    if center:
        o = o.translate((-x / 2.0, -y / 2.0, -z / 2.0))
    return o

Make a rounded_cuboid with the x, y, and z values given in size.

Parameter rounding_radius is the size of the rounded lip at top and bottom.

For segments see the documentation of Config.set_default_segments.

By default, the bottom front left corner of the rounded cuboid will be at (0,0,0). When center is True it will cause the rounded cuboid to be centered at (0,0,0).

def rounded_cylinder(height: float,
radius: float,
rounding_radius: float = 2.0,
segments: int = -1,
center: bool = False) ‑> Obj3d
Expand source code
def rounded_cylinder(
    height: float,
    radius: float,
    rounding_radius: float = 2.0,
    segments: int = -1,
    center: bool = False,
) -> Obj3d:
    """
    Make a rounded cylinder of a given radius and height.

    Parameter `rounding_radius` is the size of the rounded lip at top and bottom.

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

    By default, the rounded cylinder bottom is centered at `(0,0,0)`.
    When `center` is `True`, the rounded cylinder will centered on `(0,0,0)`.
    (In other words, the bottom of the rounded cylinder will be at `(0,0,-height/2.0`.)

    <iframe width="100%" height="250" src="examples/rounded_cylinder.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGE("segments", segments, 3)
    _chkGT("radius", radius, 0)
    rr = (
        rounded_rectangle((2 * radius, height), rounding_radius, segments)
        .translate((-radius, 0))
        .piecut(90, 270)
    )
    o3 = revolve(rr, segments=segments)
    if center:
        o3 = o3.translate((0, 0, -height / 2.0))
    return o3

Make a rounded cylinder of a given radius and height.

Parameter rounding_radius is the size of the rounded lip at top and bottom.

For segments see the documentation of Config.set_default_segments.

By default, the rounded cylinder bottom is centered at (0,0,0). When center is True, the rounded cylinder will centered on (0,0,0). (In other words, the bottom of the rounded cylinder will be at (0,0,-height/2.0.)

def sphere(radius: float, segments: int = -1) ‑> Obj3d
Expand source code
def sphere(radius: float, segments: int = -1) -> Obj3d:
    """
    Create a classical sphere of a given radius.

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

    <iframe width="100%" height="220" src="examples/sphere.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGE("radius", radius, 0)
    _chkGE("segments", segments, 3)

    circ = circle(radius, 2 * segments).piecut(90, 270)

    return revolve(circ, segments=segments)

Create a classical sphere of a given radius.

For segments see the documentation of Config.set_default_segments.

def tetrahedron(vertices: list[tuple[float, float, float]] | float | int = None) ‑> Obj3d
Expand source code
def tetrahedron(
    vertices: list[tuple[float, float, float]] | float | int = None,
) -> Obj3d:
    """
    Create a tetrahedron from 4 `vertices`.
    Or, if `vertices` is given as a single number, a regular tetrahedron of side size `vertices` is returned.
    If no vertex is given as an argument, a regular unit tetrahedron (size: 1) is returned.

    A tetrahedron is a triangular object.

    <iframe width="100%" height="220" src="examples/tetrahedron.html"></iframe>
    """
    if vertices == None or type(vertices) == float or type(vertices) == int:
        size = vertices
        if size == None:
            size = 1
        t = _m.Manifold.tetrahedron()
        if size != 1:
            t = t.scale((size, size, size))
        return Obj3d(t)
    _chkTY("vertices", vertices, list)
    if len(vertices) != 4:
        raise ValidationError(
            "You must provide exactly four vertices to define a tetrahedron."
        )

    v0, v1, v2, v3 = vertices
    # Columns describe the target tetrahedron's three edge vectors.
    linear = [Vec3.sub(v1, v0), Vec3.sub(v2, v0), Vec3.sub(v3, v0)]

    det = Mat3.determinant(linear)
    # A zero determinant means the points are coplanar or otherwise degenerate.
    if abs(det) < Const.epsilon:
        raise ValidationError(
            "Your four given points are coplanar. A flat tetrahedron is not allowed."
        )
    ccw_faces = [[0, 2, 1], [0, 1, 3], [0, 3, 2], [1, 2, 3]]
    cw_faces = [[0, 1, 2], [0, 3, 1], [0, 2, 3], [1, 3, 2]]
    if det < 0:
        faces = cw_faces
    else:
        faces = ccw_faces

    return polyhedron(vertices, faces, "none")

Create a tetrahedron from 4 vertices. Or, if vertices is given as a single number, a regular tetrahedron of side size vertices is returned. If no vertex is given as an argument, a regular unit tetrahedron (size: 1) is returned.

A tetrahedron is a triangular object.

def torus(outer_radius: float, inner_radius: float, segments=-1) ‑> Obj3d
Expand source code
def torus(outer_radius: float, inner_radius: float, segments=-1) -> Obj3d:
    """
    Create a torus with the specified radii.

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

    <iframe width="100%" height="220" src="examples/torus.html"></iframe>
    """
    if segments == -1:
        segments = Config.get_default_segments()
    _chkGT("outer_radius", outer_radius, 0)
    _chkGT("inner_radius", inner_radius, 0)
    _chkGE("segments", segments, 3)
    if inner_radius >= outer_radius:
        raise ValidationError(
            "Parameter inner_radius must be smaller than outer_radius."
        )
    sz = (outer_radius - inner_radius) / 2.0
    circ = circle(sz, segments).translate((outer_radius - sz, outer_radius - sz))

    return revolve(circ, segments=segments).translate((0, 0, -outer_radius + sz))

Create a torus with the specified radii.

For segments see the documentation of Config.set_default_segments.