Module piecad.utilities

Miscellaneous (but important) functions

Functions

def check_mesh(vertices: list[tuple[float, float, float]], faces: list[tuple[int, int, int]]) ‑> bool
Expand source code
def check_mesh(
    vertices: list[tuple[float, float, float]], faces: list[tuple[int, int, int]]
) -> bool:
    """
    Check manifold and winding of the mesh defined in vertices and faces.

    Returns true if all is well. Otherwise diagnostics will be offered and false is returned.
    """
    return _check_mesh(vertices, faces)

Check manifold and winding of the mesh defined in vertices and faces.

Returns true if all is well. Otherwise diagnostics will be offered and false is returned.

def load(filename: str) ‑> Obj2d | Obj3d
Expand source code
def load(filename: str) -> Obj3d | Obj2d:
    """
    Load a 3d object from a file.

    The format read from `filename` is determined by the file's extension.

    The available formats for 3D are:

    | Type        | Extension    |
    |:------------|:------------:|
    | 3MF         |   .3mf       |
    | GLB         |   .glb       |
    | GLTF        |   .gltf      |
    | OBJ         |   .obj       |
    | PLY         |   .ply       |
    | STL         |   .stl_ascii |
    | STL binary  |   .stl       |

    \\(See [https://github/mikedh/trimesh] for more formats.\\)

    Currently 2d objects are not supported.
    """
    dot_idx = filename.rindex(".")
    ext = filename[dot_idx + 1 :]
    mesh = trimesh.exchange.load.load(
        filename, ext, force="mesh", process=True, validate=False
    )
    if type(mesh) == trimesh.path.Path2D:
        raise ValidationError("2D objects are not currently supported.")

    return obj3d_from_vertices_and_faces(mesh.vertices, mesh.faces)

Load a 3d object from a file.

The format read from filename is determined by the file's extension.

The available formats for 3D are:

Type Extension
3MF .3mf
GLB .glb
GLTF .gltf
OBJ .obj
PLY .ply
STL .stl_ascii
STL binary .stl

(See [https://github/mikedh/trimesh] for more formats.)

Currently 2d objects are not supported.

def obj3d_from_vertices_and_faces(vertices: list[tuple[float, float, float]],
faces: list[tuple[int, int, int]],
check: str = 'none') ‑> Obj3d
Expand source code
def obj3d_from_vertices_and_faces(
    vertices: list[tuple[float, float, float]],
    faces: list[tuple[int, int, int]],
    check: str = "none",
) -> Obj3d:
    """
    Create an Obj3d instance from vertices and faces with optional mesh checking and repair.
    Used internally within the Piecad framework, but might be useful to advanced users.

    Parameters:
    - faces: List of face tuples (int, int, int)
    - vertices: List of vertex tuples (float, float, float)
    - check: One of "interactive", "batch", "repair", "none"

    Returns:
    - Obj3d instance

    Raises:
    - ValidationError if the mesh is flawed in batch mode or if manifold creation fails.
    """
    if check == "interactive":
        if not _check_mesh(vertices, faces):
            return Obj3d()
    elif check == "batch":
        msg = _quick_check_mesh(vertices, faces)
        if msg != "":
            raise ValidationError(f"Polyhedron is flawed: {msg}")
    elif check == "repair":
        mesh_output = trimesh.Trimesh(
            vertices=vertices, faces=faces, process=True, validate=True
        )
        vertices = mesh_output.vertices
        faces = mesh_output.faces
    vertices = np.array(vertices, np.float64)
    faces = np.array(faces, np.uint64)
    mesh = _m.Mesh64(vertices, faces)
    mo = _m.Manifold(mesh)
    if mo.is_empty():
        raise ValidationError(f"Error creating Manifold: {mo.status()}.")
    return Obj3d(mo)

Create an Obj3d instance from vertices and faces with optional mesh checking and repair. Used internally within the Piecad framework, but might be useful to advanced users.

Parameters: - faces: List of face tuples (int, int, int) - vertices: List of vertex tuples (float, float, float) - check: One of "interactive", "batch", "repair", "none"

Returns: - Obj3d instance

Raises: - ValidationError if the mesh is flawed in batch mode or if manifold creation fails.

def quick_check_mesh(vertices: list[tuple[float, float, float]], faces: list[tuple[int, int, int]]) ‑> str
Expand source code
def quick_check_mesh(
    vertices: list[tuple[float, float, float]], faces: list[tuple[int, int, int]]
) -> str:
    """
    Same checking as check_mesh, but no advice is offered

    Returns an empty string `""` if all is well, otherwise a short string describing the problem is
    returned.
    """
    return _quick_check_mesh(vertices, faces)

Same checking as check_mesh, but no advice is offered

Returns an empty string "" if all is well, otherwise a short string describing the problem is returned.

def save(filename: str,
*objs: Obj2d | Obj3d) ‑> None
Expand source code
def save(filename: str, *objs: Obj3d | Obj2d) -> None:
    """
    Save a 3d or 2d object in a file suitable for printing, etc.

    If [p filename] does not contain a path separator character (`'/'` or `'\\'`) the
    `Downloads` directory for you platform is prepended. Thus if you set
    [p filename] to `output.obj`, the place where the file is saved is
    `c:\\Users\\username\\Downloads\\output.obj` on Windows or
    `/home/username/Downloads/output.obj` on Linux and MacOS.
    If you want a file saved in the current directory, prepend `'./'` or `'.\\'`.

    You can override the `Downloads` directory by setting the global environment variable
    `PIECAD_SAVE_DIR`.

    The model format placed in [p:filename] is determined by the file's extension.

    The available formats for 3D are:

    | Type        | Extension    |
    |:------------|:------------:|
    | 3MF         |   .3mf       |
    | GLB         |   .glb       |
    | GLTF        |   .gltf      |
    | OBJ         |   .obj       |
    | PLY         |   .ply       |
    | STL         |   .stl_ascii |
    | STL binary  |   .stl       |

    \\(See [https://github/mikedh/trimesh] for more formats.\\)

    For 2D, only the SVG (.svg) format is available.
    """

    if filename.find("/") == -1 and filename.find("\\") == -1:
        filename = str(_Path(_get_save_dir()) / filename)
        print("Saving: " + filename)
    _chkGE("len(objs)", len(objs), 1)
    dot_idx = filename.rindex(".")
    ext = filename[dot_idx + 1 :]
    if type(objs[0]) == Obj3d:
        if len(objs) == 1:
            obj = objs[0]
            if filename.endswith(".3mf"):
                _export_3mf(
                    filename,
                    obj.mo,
                    Obj3d.color_map,
                    Config.get_default_units(),
                    Config.get_default_color(),
                )
                return
            mesh = obj.mo.to_mesh64()
            if mesh.vert_properties.shape[1] > 3:
                vertices = mesh.vert_properties[:, :3]
            else:
                vertices = mesh.vert_properties

            face_colors = _face_colors(mesh)
            mesh_output = trimesh.Trimesh(
                vertices=vertices,
                faces=mesh.tri_verts,
                face_colors=face_colors,
                process=True,
                validate=False,
            )
            # Manifold3d has a different definition than Trimesh
            if not mesh_output.is_watertight:
                print("WARNING: output mesh is not watertight")
            trimesh.exchange.export.export_mesh(mesh_output, filename, ext)
        else:
            scene = trimesh.Scene()
            for obj in objs:
                mesh = obj.mo.to_mesh64()
                if mesh.vert_properties.shape[1] > 3:
                    vertices = mesh.vert_properties[:, :3]
                else:
                    vertices = mesh.vert_properties
                face_colors = _face_colors(mesh)
                mesh_output = trimesh.Trimesh(
                    vertices=vertices,
                    faces=mesh.tri_verts,
                    face_colors=face_colors,
                    process=True,
                    validate=False,
                )
                # Manifold3d has a different definition than Trimesh
                if not mesh_output.is_watertight:
                    print("WARNING: output mesh is not watertight")
                scene.add_geometry(mesh_output)
            if filename.endswith(".3mf"):
                s_mesh = scene.to_geometry()
                s_vertices = np.array(s_mesh.vertices, np.float64)
                s_faces = np.array(s_mesh.faces, np.uint64)
                mo = _m.Manifold(_m.Mesh64(s_vertices, s_faces))
                _export_3mf(
                    filename,
                    mo,
                    Obj3d.color_map,
                    Config.get_default_units(),
                    Config.get_default_color(),
                )
                return
            trimesh.exchange.export.export_scene(scene, filename, ext)
        # trimesh obj file export does not end with newline
        # currently this upsets prusa_slicer
        if ext == "obj":
            with open(filename, "a") as f:
                f.write("\n")
    else:  # Obj2d
        if ext != "svg":
            raise (ValidationError("Only the SVG format is supported for Obj2d."))
        _save_svg(filename, *objs)

Save a 3d or 2d object in a file suitable for printing, etc.

If [p filename] does not contain a path separator character ('/' or '\') the Downloads directory for you platform is prepended. Thus if you set [p filename] to output.obj, the place where the file is saved is c:\Users\username\Downloads\output.obj on Windows or /home/username/Downloads/output.obj on Linux and MacOS. If you want a file saved in the current directory, prepend './' or '.\'.

You can override the Downloads directory by setting the global environment variable PIECAD_SAVE_DIR.

The model format placed in [p:filename] is determined by the file's extension.

The available formats for 3D are:

Type Extension
3MF .3mf
GLB .glb
GLTF .gltf
OBJ .obj
PLY .ply
STL .stl_ascii
STL binary .stl

(See [https://github/mikedh/trimesh] for more formats.)

For 2D, only the SVG (.svg) format is available.

def view(obj: Obj2d | Obj3d,
title: str = '') ‑> None
Expand source code
def view(obj: Obj3d | Obj2d, title: str = "") -> None:
    """
    Use `Piecad-Viewer` to display the geometry object.

    Returns obj unchanged... so that it works well in return statements.

    If you have trouble viewing and are using a graphical debugger,
    see the `view_all_now()` function below.

    ```
    return union(o1, o2, o3)
    # can be displayed in 3 parts and the whole object, like this:
    return view(union(view(o1), view(o2), view(o3)))
    ```

    """
    global _view_meshes, _view_meshes_titles

    _chkGO("obj", obj)

    if title == "":
        title = _info_str("view")

    if type(obj) == Obj2d:
        color = obj._color if obj._color is not None else Config.get_default_color()
        obj = Obj3d(_m.Manifold.extrude(obj.mo, 0.1)).color(color)

    mesh = obj.mo.to_mesh64()
    if mesh.vert_properties.shape[1] > 3:
        vertices = mesh.vert_properties[:, :3]
    else:
        vertices = mesh.vert_properties
    faces = mesh.tri_verts
    mesh_output = trimesh.Trimesh(
        vertices=vertices,
        faces=faces,
        face_colors=_face_colors(mesh),
        process=True,
        validate=False,
    )
    if len(_view_meshes) == 0:
        atexit.register(_wait_for_view_handler_exit)
    _view_meshes.append(mesh_output)
    _view_meshes_titles.append(title)
    return obj

Use Piecad-Viewer to display the geometry object.

Returns obj unchanged… so that it works well in return statements.

If you have trouble viewing and are using a graphical debugger, see the view_all_now() function below.

return union(o1, o2, o3)
# can be displayed in 3 parts and the whole object, like this:
return view(union(view(o1), view(o2), view(o3)))
def view_all_now() ‑> None
Expand source code
def view_all_now() -> None:
    """
    The `view()` function merely records a list of objects to be displayed.
    By default, a function to view the meshes is called from `atexit`.
    Unfortunately a number of graphical debuggers (e.g. Visual Studio Code
    and PyCharm) have a short timeout for `atexit` functions.
    To avoid this issue, use `view_all_now()` at the end of your script to
    display all objects recorded by `view()` with no timeout.
    Alternatively, run the script without debugging.
    """
    from ._viewer import show_meshes

    v = show_meshes(_view_meshes, _view_meshes_titles)
    _viewer_closed_event.wait()
    v.clear()
    _view_meshes.clear()
    _view_meshes_titles.clear()
    _viewer_closed_event.clear()

The view() function merely records a list of objects to be displayed. By default, a function to view the meshes is called from atexit. Unfortunately a number of graphical debuggers (e.g. Visual Studio Code and PyCharm) have a short timeout for atexit functions. To avoid this issue, use view_all_now() at the end of your script to display all objects recorded by view() with no timeout. Alternatively, run the script without debugging.

def winding(lt: list[tuple[float, float]]) ‑> str
Expand source code
def winding(lt: list[tuple[float, float]]) -> str:
    """
    String description of winding of a 2D polygon.

    Returns one of `"cw"`, `"ccw"`, `"zero"` or `"too small"`.
    """

    def wstr(winding):
        if winding > 0:
            return "cw"
        if winding < 0:
            return "ccw"
        return "zero"

    length = len(lt)
    if length < 3:
        return "too small"
    winding = 0.0
    for i in range(0, length):
        winding += (lt[(i + 1) % length][0] - lt[i][0]) * (
            lt[(i + 1) % length][1] + lt[i][1]
        )
    return wstr(winding)

String description of winding of a 2D polygon.

Returns one of "cw", "ccw", "zero" or "too small".