Module voxelfuse.mesh

Mesh Class

Initialized from a voxel model


Copyright 2021 - Cole Brauer, Dan Aukes

Expand source code
"""
Mesh Class

Initialized from a voxel model

----

Copyright 2021 - Cole Brauer, Dan Aukes
"""

import sys
import numpy as np
import meshio
import k3d
import mcubes
from quad_mesh_simplify import simplify_mesh
from typing import Union as TypeUnion, Tuple
from numba import njit
from tqdm import tqdm

import PyQt5.QtWidgets as qtw
import PyQt5.QtGui as qg
import pyqtgraph.opengl as pgo

from voxelfuse.voxel_model import VoxelModel, rgb_to_hex
from voxelfuse.materials import material_properties

class Mesh:
    """
    Mesh object that can be exported or passed to a Plot object.
    """

    def __init__(self, voxels: TypeUnion[np.ndarray, None], verts: np.ndarray, verts_colors: np.ndarray, tris: np.ndarray, resolution: float):
        """
        Initialize a Mesh object.

        Args:
            voxels: Voxel data array
            verts: List of coordinates of surface vertices
            verts_colors: List of colors associated with each vertex
            tris: List of the sets of vertices associated with triangular faces
            resolution: Number of voxels per mm
        """
        if voxels is not None:
            self.model = voxels
        else:
            self.model = np.array([[[0]]])

        self.verts = verts
        self.colors = verts_colors
        self.tris = tris
        self.res = resolution

    @classmethod
    def fromMeshFile(cls, filename: str, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
        """
        Import a mesh file to a mesh object.

        ----

        Example:

        ``mesh1 = vf.Mesh.fromMeshFile(example.stl)``

        ----

        Args:
            filename: File name with extension
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            Mesh
        """
        # Open file
        data = meshio.read(filename)

        # Read verts
        verts = np.array(data.points)

        # Align to origin
        x_min = np.min(verts[:, 0])
        y_min = np.min(verts[:, 1])
        z_min = np.min(verts[:, 2])
        verts[:, 0] = np.subtract(verts[:, 0], x_min)
        verts[:, 1] = np.subtract(verts[:, 1], y_min)
        verts[:, 2] = np.subtract(verts[:, 2], z_min)

        # Generate colors
        verts_colors = generateColors(len(verts), color)

        # Read tris
        tris = []
        for cell in data.cells:
            if cell[0] == 'triangle':
                for tri in cell[1]:
                    tris.append(tri)
        tris = np.array(tris)

        return cls(None, verts, verts_colors, tris, 1)

    @classmethod
    def fromVoxelModel(cls, voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None):
        """
        Generate a mesh object from a VoxelModel object.

        ----

        Example:

        ``mesh1 = vf.Mesh.fromVoxelModel(model1)``

        ----

        Args:
            voxel_model: VoxelModel object to be converted to a mesh
            color: Mesh color in the format (r, g, b, a), None to use voxel colors
        
        Returns:
            Mesh
        """
        voxel_model_fit = voxel_model.fitWorkspace()
        voxel_model_array = voxel_model_fit.voxels.astype(np.uint16)
        model_materials = voxel_model_fit.materials
        model_offsets = voxel_model_fit.coords

        # Find exterior voxels
        exterior_voxels_array = voxel_model_fit.difference(voxel_model_fit.erode(radius=1, connectivity=1)).voxels
        
        x_len, y_len, z_len = voxel_model_array.shape
        
        # Create list of exterior voxel coordinates
        exterior_voxels_coords = []
        for x in tqdm(range(x_len), desc='Finding exterior voxels'):
            for y in range(y_len):
                for z in range(z_len):
                    if exterior_voxels_array[x, y, z] != 0:
                        exterior_voxels_coords.append([x, y, z])

        # Get voxel array
        voxel_model_array[voxel_model_array < 0] = 0

        # Initialize arrays
        verts = []
        verts_colors = []
        verts_indices = np.zeros((x_len+1, y_len+1, z_len+1))
        tris = []
        vi = 1  # Tracks current vertex index

        # Loop through voxel_model_array data
        for voxel_coords in tqdm(exterior_voxels_coords, desc='Meshing'):
            x, y, z = voxel_coords

            if color is None:
                r = 0
                g = 0
                b = 0

                for i in range(voxel_model.materials.shape[1]-1):
                    r = r + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['r']
                    g = g + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['g']
                    b = b + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['b']

                r = 1 if r > 1 else r
                g = 1 if g > 1 else g
                b = 1 if b > 1 else b

                a = 1 - model_materials[voxel_model_array[x, y, z]][1]

                voxel_color = [r, g, b, a]
            else:
                voxel_color = list(color)

            # Add cube vertices
            new_verts, verts_indices, new_tris, vi = addVerticesAndTriangles(voxel_model_array, verts_indices, model_offsets, x, y, z, vi)
            verts += new_verts
            tris += new_tris

            # Apply color to all vertices
            for i in range(len(new_verts)):
                verts_colors.append(voxel_color)

        verts = np.array(verts, dtype=np.float32)
        verts_colors = np.array(verts_colors, dtype=np.float32)
        tris = np.array(tris, dtype=np.uint32)

        return cls(voxel_model_array, verts, verts_colors, tris, voxel_model.resolution)

    @classmethod
    def simpleSquares(cls, voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None):
        """
        Generate a mesh object from a VoxelModel object using large square faces.

        This function can greatly reduce the file size of generated meshes. However, it may not correctly recognize
        small (1 voxel) model features and currently produces files with a nonstandard vertex arrangement. Use at your own
        risk.

        ----

        Example:

        ``mesh1 = vf.Mesh.simpleSquares(model1)``

        ----

        Args:
            voxel_model: VoxelModel object to be converted to a mesh
            color: Mesh color in the format (r, g, b, a), None to use voxel colors
        
        Returns:
            Mesh
        """
        voxel_model_fit = voxel_model.fitWorkspace()
        voxel_model_array = voxel_model_fit.voxels.astype(np.uint16)
        model_materials = voxel_model_fit.materials
        model_offsets = voxel_model_fit.coords

        x_len, y_len, z_len = voxel_model_array.shape

        # Determine vertex types
        vert_type = np.zeros((x_len + 1, y_len + 1, z_len + 1), dtype=np.uint8)
        vert_color = np.zeros((x_len + 1, y_len + 1, z_len + 1, 4), dtype=np.float32)
        for x in tqdm(range(x_len), desc='Finding voxel vertices'):
            for y in range(y_len):
                for z in range(z_len):
                    if voxel_model_array[x, y, z] > 0:
                        vert_type[x:x+2, y:y+2, z:z+2] = 1 # Type 1 = occupied/exterior

                        if color is None:
                            r = 0
                            g = 0
                            b = 0

                            for i in range(voxel_model.materials.shape[1] - 1):
                                r = r + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['r']
                                g = g + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['g']
                                b = b + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['b']

                            r = 1 if r > 1 else r
                            g = 1 if g > 1 else g
                            b = 1 if b > 1 else b

                            a = 1 - model_materials[voxel_model_array[x, y, z]][1]

                            voxel_color = np.array([r, g, b, a])
                        else:
                            voxel_color = np.array(color)

                        for cx in range(x, x+2):
                            for cy in range(y, y+2):
                                for cz in range(z, z+2):
                                    vert_color[cx, cy, cz, :] = voxel_color

        for x in tqdm(range(1, x_len), desc='Finding interior vertices'):
            for y in range(1, y_len):
                for z in range(1, z_len):
                    vert_type = markInterior(vert_type, x, y, z)

        for x in tqdm(range(0, x_len+1), desc='Finding feature vertices'):
            for y in range(0, y_len+1):
                for z in range(0, z_len+1):
                    vert_type = markInsideCorner(vert_type, x, y, z)

        # Initialize arrays
        vi = 0 # Tracks current vertex index
        verts = []
        colors = []
        tris = []
        quads = []
        vert_index = np.multiply(np.ones_like(vert_type, dtype=np.int32), -1)

        for x in tqdm(range(x_len + 1), desc='Meshing'):
            for y in range(y_len + 1):
                for z in range(z_len + 1):
                    dirs = [[1, 1, 0], [1, 0, 1], [0, 1, 1]]
                    for d in dirs:
                        vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads = findSquare(vi, vert_type, vert_index, vert_color, voxel_model_array, x, y, z, d[0], d[1], d[2])
                        verts += new_verts
                        colors += new_colors
                        tris += new_tris
                        quads += new_quads

        verts = np.array(verts, dtype=np.float32)
        colors = np.array(colors, dtype=np.float32)
        tris = np.array(tris, dtype=np.uint32)
        quads = np.array(quads, dtype=np.uint32)

        # Shift model to align with origin
        verts[:, 0] = np.add(verts[:, 0], model_offsets[0])
        verts[:, 1] = np.add(verts[:, 1], model_offsets[1])
        verts[:, 2] = np.add(verts[:, 2], model_offsets[2])

        return cls(voxel_model_array, verts, colors, tris, voxel_model.resolution)

    @classmethod
    def marchingCubes(cls, voxel_model: VoxelModel, smooth: bool = False, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
        """
        Generate a mesh object from a VoxelModel object using a marching cubes algorithm.

        This meshing approach is best suited to high resolution models where some smoothing is acceptable.

        Args:
            voxel_model: VoxelModel object to be converted to a mesh
            smooth: Enable smoothing
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            None
        """
        voxel_model_fit = voxel_model.fitWorkspace().getOccupied()
        voxels = voxel_model_fit.voxels.astype(np.uint16)
        x, y, z = voxels.shape
        model_offsets = voxel_model_fit.coords

        voxels_padded = np.zeros((x + 2, y + 2, z + 2))
        voxels_padded[1:-1, 1:-1, 1:-1] = voxels

        if smooth:
            voxels_padded = mcubes.smooth(voxels_padded)
            levelset = 0
        else:
            levelset = 0.5

        verts, tris = mcubes.marching_cubes(voxels_padded, levelset)

        # Shift model to align with origin
        verts = np.subtract(verts, 0.5)
        verts[:, 0] = np.add(verts[:, 0], model_offsets[0])
        verts[:, 1] = np.add(verts[:, 1], model_offsets[1])
        verts[:, 2] = np.add(verts[:, 2], model_offsets[2])

        verts_colors = generateColors(len(verts), color)

        return cls(voxels_padded, verts, verts_colors, tris, voxel_model.resolution)

    @classmethod
    def copy(cls, mesh):
        """
        Initialize a Mesh that is a copy of another mesh.

        Args:
            mesh: Reference Mesh object
        
        Returns:
            Mesh
        """
        new_mesh = cls(np.copy(mesh.model), np.copy(mesh.verts), np.copy(mesh.colors), np.copy(mesh.tris), mesh.res)
        return new_mesh

    def setResolution(self, resolution: float):
        """
        Change the defined resolution of a mesh.

        The mesh resolution will determine the scale of plots and exported mesh files.

        Args:
            resolution: Number of voxels per mm (higher number = finer resolution)
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.res = resolution
        return new_mesh

    def scale(self, factor: float):
        """
        Apply a scaling factor to a mesh.

        Args:
            factor: Scaling factor
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.verts = np.multiply(self.verts, factor)
        return new_mesh

    def simplify(self, percent_verts: float, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
        """
        Simplify a mesh to contain a given percentage of the original number of vertices.

        More information on the simplification algorithm is available at: https://github.com/jannessm/quadric-mesh-simplification

        Args:
            percent_verts: Percentage of vertex count allowed in the result mesh, 0-1
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            Mesh
        """
        num_verts = self.verts.shape[0]
        target_verts = num_verts * percent_verts

        new_verts, new_tris = simplify_mesh(positions=self.verts.astype(np.double), face=self.tris.astype(np.uint32), num_nodes=target_verts)
        verts_colors = generateColors(len(new_verts), color)

        return Mesh(np.copy(self.model), new_verts, verts_colors, new_tris, self.res)

    def translate(self, vector: Tuple[float, float, float]):
        """
        Translate a model by the specified vector.

        Args:
            vector: Translation vector in voxels
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.verts[:, 0] = np.add(self.verts[:, 0], vector[0])
        new_mesh.verts[:, 1] = np.add(self.verts[:, 1], vector[1])
        new_mesh.verts[:, 2] = np.add(self.verts[:, 2], vector[2])
        return new_mesh

    def translateMM(self, vector: Tuple[float, float, float]):
        """
        Translate a model by the specified vector.

        Args:
            vector: Translation vector in mm
        
        Returns:
            Mesh
        """
        xV = vector[0] * self.res
        yV = vector[1] * self.res
        zV = vector[2] * self.res
        new_mesh = self.translate((xV, yV, zV))
        return new_mesh

    def setColor(self, color: Tuple[float, float, float, float]):
        """
        Change the color of a mesh.

        Args:
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.colors = generateColors(len(self.verts), color)
        return new_mesh

    def plot(self, plot = None, name: str = 'mesh', wireframe: bool = True, mm_scale: bool = False, **kwargs):
        """
        Add mesh to a K3D plot in Jupyter Notebook.

        Additional display options:

        - flat_shading: `bool`. Whether mesh should display with flat shading.
        - opacity: `float`. Opacity of mesh.
        - volume: `array_like`. 3D array of `float`
        - volume_bounds: `array_like`. 6-element tuple specifying the bounds of the volume data (x0, x1, y0, y1, z0, z1)
        - opacity_function: `array`. A list of float tuples (attribute value, opacity), sorted by attribute value. The first tuples should have value 0.0, the last 1.0; opacity is in the range 0.0 to 1.0.
        - side: `string`. Control over which side to render for a mesh. Legal values are `front`, `back`, `double`.
        - texture: `bytes`. Image data in a specific format.
        - texture_file_format: `str`. Format of the data, it should be the second part of MIME format of type 'image/', for example 'jpeg', 'png', 'gif', 'tiff'.
        - uvs: `array_like`. Array of float uvs for the texturing, coresponding to each vertex.
        - kwargs: `dict`. Dictionary arguments to configure transform and model_matrix.

        More information available at: https://github.com/K3D-tools/K3D-jupyter

        Args:
            plot: Plot object to add mesh to
            name: Mesh name
            wireframe: Enable displaying mesh as a wireframe
            mm_scale: Enable to use a mm plot scale, disable to use a voxel plot scale
            kwargs: Additional display options (see above)
        
        Returns:
            K3D plot object
        """
        # Get verts
        verts = self.verts

        # Adjust coordinate scale
        if mm_scale:
            verts = np.divide(verts, self.res)

        # Get tris
        tris = self.tris

        # Get colors
        colors = []
        for c in self.colors:
            colors.append(rgb_to_hex(c[0], c[1], c[2]))
        colors = np.array(colors, dtype=np.uint32)

        # Plot
        if plot is None:
            plot = k3d.plot()

        plot += k3d.mesh(verts.astype(np.float32), tris.astype(np.uint32), colors=colors, name=name, wireframe=wireframe, **kwargs)
        return plot

    def viewer(self, grids: bool = False, drawEdges: bool = True,
               edgeColor: Tuple[float, float, float, float] = (0, 0, 0, 0.5),
               positionOffset: Tuple[int, int, int] = (0, 0, 0), viewAngle: Tuple[int, int, int] = (40, 30, 300),
               resolution: Tuple[int, int] = (1280, 720), name: str = 'Plot 1', export: bool = False):
        """
        Display the mesh in a 3D viewer window.

        This function will block program execution until viewer window is closed

        Args:
            grids: Enable/disable display of XYZ axes and grids
            drawEdges: Enable/disable display of voxel edges
            edgeColor: Set display color of voxel edges
            positionOffset: Offset of the camera target from the center of the model in voxels
            viewAngle: Elevation, Azimuth, and Distance of the camera
            resolution: Window resolution in px
            name: Plot window name
            export: Enable/disable exporting a screenshot of the plot
        
        Returns:
            None
        """
        app = qtw.QApplication(sys.argv)

        mesh_data = pgo.MeshData(vertexes=self.verts, faces=self.tris, vertexColors=self.colors, faceColors=None)
        mesh_item = pgo.GLMeshItem(meshdata=mesh_data, shader='balloon', drawEdges=drawEdges, edgeColor=edgeColor,
                                   smooth=False, computeNormals=False, glOptions='translucent')

        widget = pgo.GLViewWidget()
        widget.setBackgroundColor('w')
        widget.addItem(mesh_item)

        if grids:
            # Add grids
            gx = pgo.GLGridItem()
            gx.setSize(x=50, y=50, z=50)
            gx.rotate(90, 0, 1, 0)
            gx.translate(-0.5, 24.5, 24.5)
            widget.addItem(gx)
            gy = pgo.GLGridItem()
            gy.setSize(x=50, y=50, z=50)
            gy.rotate(90, 1, 0, 0)
            gy.translate(24.5, -0.5, 24.5)
            widget.addItem(gy)
            gz = pgo.GLGridItem()
            gz.setSize(x=50, y=50, z=50)
            gz.translate(24.5, 24.5, -0.5)
            widget.addItem(gz)

            # Add axes
            ptsx = np.array([[-0.5, -0.5, -0.5], [50, -0.5, -0.5]])
            pltx = pgo.GLLinePlotItem(pos=ptsx, color=(1, 0, 0, 1), width=1, antialias=True)
            widget.addItem(pltx)
            ptsy = np.array([[-0.5, -0.5, -0.5], [-0.5, 50, -0.5]])
            plty = pgo.GLLinePlotItem(pos=ptsy, color=(0, 1, 0, 1), width=1, antialias=True)
            widget.addItem(plty)
            ptsz = np.array([[-0.5, -0.5, -0.5], [-0.5, -0.5, 50]])
            pltz = pgo.GLLinePlotItem(pos=ptsz, color=(0, 0, 1, 1), width=1, antialias=True)
            widget.addItem(pltz)

        # Set plot options
        widget.opts['center'] = qg.QVector3D(((self.model.shape[0] / self.res) / 2) + positionOffset[0],
                                             ((self.model.shape[1] / self.res) / 2) + positionOffset[1],
                                             ((self.model.shape[2] / self.res) / 2) + positionOffset[2])
        widget.opts['elevation'] = viewAngle[0]
        widget.opts['azimuth'] = viewAngle[1]
        widget.opts['distance'] = viewAngle[2]
        widget.resize(resolution[0], resolution[1])

        # Show plot
        widget.setWindowTitle(str(name))
        widget.show()

        app.processEvents()

        # if export: # TODO: Fix export code
        #     widget.paintGL()
        #     widget.grabFrameBuffer().save(str(name) + '.png')

        print('Close viewer to resume program')
        app.exec_()
        app.quit()

    # Export model from mesh data
    def export(self, filename: str):
        """
        Save a copy of the mesh with the specified name and file format.

        ----

        Example:

        ``mesh1.export('result.stl')``

        ----

        Args:
            filename: File name with extension
        
        Returns:
            None
        """
        # Adjust coordinate scale
        verts = np.divide(self.verts, self.res)

        cells = {
            "triangle": self.tris
        }

        output_mesh = meshio.Mesh(verts, cells)
        meshio.write(filename, output_mesh)

# Helper functions ##############################################################
def generateColors(n: int, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
    """
    Generate a colors list with the given number of elements

    Args:
        n: Number of vertices in target model
        color: Mesh color in the format (r, g, b, a)
    
    Returns:
        List of vertex colors
    """
    verts_colors = []
    voxel_color = list(color)
    for i in range(n):
        verts_colors.append(voxel_color)
    verts_colors = np.array(verts_colors)
    return verts_colors

@njit()
def check_adjacent(input_model: np.ndarray, x_coord: int, y_coord: int, z_coord: int, x_dir: int, y_dir: int, z_dir: int):
    """
    Check if a target voxel has another voxel adjacent to it in the specified direction.

    Args:
        input_model: VoxelModel.voxels
        x_coord: Target voxel X location
        y_coord: Target voxel Y location
        z_coord: Target voxel Z location
        x_dir: Specify X direction and distance (usually 1 or -1)
        y_dir: Specify Y direction and distance (usually 1 or -1)
        z_dir: Specify Z direction and distance (usually 1 or -1)
    
    Returns:
        Adjacent voxel present/not present
    """
    x_coord_new = x_coord+x_dir
    y_coord_new = y_coord+y_dir
    z_coord_new = z_coord+z_dir

    x_in_bounds = (x_coord_new >= 0) and (x_coord_new < input_model.shape[0])
    y_in_bounds = (y_coord_new >= 0) and (y_coord_new < input_model.shape[1])
    z_in_bounds = (z_coord_new >= 0) and (z_coord_new < input_model.shape[2])

    if x_in_bounds and y_in_bounds and z_in_bounds and input_model[x_coord_new, y_coord_new, z_coord_new] > 0:
        return True
    else:
        return False

@njit()
def addVerticesAndTriangles(voxel_model_array: np.ndarray, verts_indices: np.ndarray, model_offsets: Tuple, x: int, y: int, z: int, vi: int):
    """
    Find the applicable mesh vertices and triangles for a target voxel.

    Args:
        voxel_model_array: VoxelModel.voxels
        verts_indices: verts indices array
        model_offsets: VoxelModel.coords
        x: Target voxel X location
        y: Target voxel Y location
        z: Target voxel Z location
        vi: Current vertex index

    Returns:
        New verts, Updated verts indices array, New tris, Updated current vert index
    """
    adjacent = [
        [check_adjacent(voxel_model_array, x, y, z, 1, 0, 0), check_adjacent(voxel_model_array, x, y, z, -1, 0, 0)],
        [check_adjacent(voxel_model_array, x, y, z, 0, 1, 0), check_adjacent(voxel_model_array, x, y, z, 0, -1, 0)],
        [check_adjacent(voxel_model_array, x, y, z, 0, 0, 1), check_adjacent(voxel_model_array, x, y, z, 0, 0, -1)]
    ]

    cube_verts_indices = np.array([0, 0, 0, 0, 0, 0, 0, 0])
    verts = []
    tris = []

    if not adjacent[0][0] or not adjacent[1][0] or not adjacent[2][0]:
        vert_pos = (x+1, y+1, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[0] = verts_indices[vert_pos]

    if not adjacent[0][0] or not adjacent[1][1] or not adjacent[2][0]:
        vert_pos = (x+1, y, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[1] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][0] or not adjacent[2][0]:
        vert_pos = (x, y+1, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[2] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][1] or not adjacent[2][0]:
        vert_pos = (x, y, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[3] = verts_indices[vert_pos]

    if not adjacent[0][0] or not adjacent[1][0] or not adjacent[2][1]:
        vert_pos = (x+1, y+1, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[4] = verts_indices[vert_pos]

    if not adjacent[0][0] or not adjacent[1][1] or not adjacent[2][1]:
        vert_pos = (x+1, y, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[5] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][0] or not adjacent[2][1]:
        vert_pos = (x, y+1, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[6] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][1] or not adjacent[2][1]:
        vert_pos = (x, y, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[7] = verts_indices[vert_pos]

    if not adjacent[0][0]:
        tris.append([cube_verts_indices[0] - 1, cube_verts_indices[1] - 1, cube_verts_indices[5] - 1])
        tris.append([cube_verts_indices[5] - 1, cube_verts_indices[4] - 1, cube_verts_indices[0] - 1])

    if not adjacent[1][0]:
        tris.append([cube_verts_indices[2] - 1, cube_verts_indices[0] - 1, cube_verts_indices[4] - 1])
        tris.append([cube_verts_indices[4] - 1, cube_verts_indices[6] - 1, cube_verts_indices[2] - 1])

    if not adjacent[2][0]:
        tris.append([cube_verts_indices[1] - 1, cube_verts_indices[0] - 1, cube_verts_indices[2] - 1])
        tris.append([cube_verts_indices[2] - 1, cube_verts_indices[3] - 1, cube_verts_indices[1] - 1])

    if not adjacent[0][1]:
        tris.append([cube_verts_indices[3] - 1, cube_verts_indices[2] - 1, cube_verts_indices[6] - 1])
        tris.append([cube_verts_indices[6] - 1, cube_verts_indices[7] - 1, cube_verts_indices[3] - 1])

    if not adjacent[1][1]:
        tris.append([cube_verts_indices[1] - 1, cube_verts_indices[3] - 1, cube_verts_indices[7] - 1])
        tris.append([cube_verts_indices[7] - 1, cube_verts_indices[5] - 1, cube_verts_indices[1] - 1])

    if not adjacent[2][1]:
        tris.append([cube_verts_indices[4] - 1, cube_verts_indices[5] - 1, cube_verts_indices[7] - 1])
        tris.append([cube_verts_indices[7] - 1, cube_verts_indices[6] - 1, cube_verts_indices[4] - 1])

    return verts, verts_indices.astype(np.uint32), tris, vi

@njit()
def markInterior(vert_type: np.ndarray, x: int, y: int, z: int):
    """
    Determine if target voxel is an interior voxel.

    Args:
        vert_type: Array of vertex types
        x: Target voxel X
        y: Target voxel Y
        z: Target voxel Z

    Returns:
        Updated array of vertex types
    """
    if np.all(vert_type[x-1:x+2, y-1:y+2, z-1:z+2] != 0):
        vert_type[x, y, z] = 3 # Type 3 = interior/already included in a square
    return vert_type

@njit()
def markInsideCorner(vert_type, x, y, z):
    """
    Determine if target voxel is an inside corner.

    Args:
        vert_type: Array of vertex types
        x: Target voxel X
        y: Target voxel Y
        z: Target voxel Z

    Returns:
        Updated array of vertex types
    """
    x_len, y_len, z_len = vert_type.shape
    adjacent_empty = [True, True, True, True, True, True]

    if x > 0:
        adjacent_empty[0] = (vert_type[x - 1, y, z] == 0)
    if x < x_len-1:
        adjacent_empty[1] = (vert_type[x + 1, y, z] == 0)

    if y > 0:
        adjacent_empty[2] = (vert_type[x, y - 1, z] == 0)
    if y < y_len-1:
        adjacent_empty[3] = (vert_type[x, y + 1, z] == 0)

    if z > 0:
        adjacent_empty[4] = (vert_type[x, y, z - 1] == 0)
    if z < z_len-1:
        adjacent_empty[5] = (vert_type[x, y, z + 1] == 0)

    is_face = [adjacent_empty[0] != adjacent_empty[1],
               adjacent_empty[2] != adjacent_empty[3],
               adjacent_empty[4] != adjacent_empty[5]]

    # Inside corner  - 0 faces
    # Face           - 1 face
    # Outside edge   - 2 faces
    # Outside corner - 3 faces
    if vert_type[x, y, z] == 1 and np.sum(np.array(is_face)) == 0:
        vert_type[x, y, z] = 2  # Type 2 = blocking

    return vert_type

@njit()
def findSquare(vi: int, vert_type: np.ndarray, vert_index: np.ndarray, vert_color: np.ndarray, voxel_model_array: np.ndarray, x: int, y: int, z: int, dx: int, dy: int, dz: int):
    """
    Find the largest square starting from a given point and generate the corresponding points and tris.

    Args:
        vi: Current vertex index
        vert_type: Array of vertex types
        vert_index: Array of vertex indices
        vert_color: Array of vertex colors
        voxel_model_array: Voxel data array
        x: Target voxel X
        y: Target voxel Y
        z: Target voxel Z
        dx: Square search step in X
        dy: Square search step in Y
        dz: Square search step in Z

    Returns:
        vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads
    """
    x_len, y_len, z_len = vert_type.shape
    new_verts = []
    new_colors = []
    new_tris = []
    new_quads = []

    vert_on_surface = (vert_type[x, y, z] == 1 or vert_type[x, y, z] == 2)
    if vert_on_surface and x+dx < x_len and y+dy < y_len and z+dz < z_len:  # Point is a face vertex and next point is in bounds
        xn = x
        yn = y
        zn = z

        for i in range(1, max(x_len - x, y_len - y, z_len - z)):  # See if a square can be found starting at this point
            xn = x + dx * i
            yn = y + dy * i
            zn = z + dz * i

            # Check if endpoint is in bounds
            in_range = [xn < x_len,
                        yn < y_len,
                        zn < z_len]

            if not np.all(np.array(in_range)):
                xn = x + dx * (i-1)
                yn = y + dy * (i-1)
                zn = z + dz * (i-1)
                break

            face = (vert_type[x:xn+1, y:yn+1,  z:zn+1] == 1)
            blocking = (vert_type[x:xn+1, y:yn+1,  z:zn+1] == 2)
            on_surface = np.logical_or(face, blocking)

            # Check if square includes only surface vertices
            if not np.all(on_surface):
                xn = x + dx * (i-1)
                yn = y + dy * (i-1)
                zn = z + dz * (i-1)
                break

            # Check if square includes any blocking vertices
            if np.any(blocking):
                break

        square = None

        # Determine vert coords based on search direction
        if xn > x and yn > y and zn == z:
            # vert_type[x:xn+1, y:yn+1, z] = 1 # Type 1 = occupied/exterior
            vert_type[x+1:xn, y+1:yn, z] = 3 # Type 3 = interior/already included in a square

            vx_pos = False
            vx_neg = False
            if z - 1 >= 0:
                vx_neg = (voxel_model_array[x, y, z - 1] != 0)
            if z < z_len-1:
                vx_pos = (voxel_model_array[x, y, z] != 0)

            if vx_pos and not vx_neg: # CW
                square = [[x, y, z],
                          [x, yn, z],
                          [xn, y, z],
                          [xn, yn, z]]
            elif vx_neg and not vx_pos: # CCW
                square = [[x, y, z],
                          [xn, y, z],
                          [x, yn, z],
                          [xn, yn, z]]
            else: # Interior face -- can occur with certain small features
                square = None

        elif xn > x and yn == y and zn > z:
            # vert_type[x:xn+1, y, z:zn+1] = 1 # Type 1 = occupied/exterior
            vert_type[x+1:xn, y, z+1:zn] = 3 # Type 3 = interior/already included in a square

            vx_pos = False
            vx_neg = False
            if y - 1 >= 0:
                vx_neg = (voxel_model_array[x, y - 1, z] != 0)
            if y < y_len-1:
                vx_pos = (voxel_model_array[x, y, z] != 0)

            if vx_pos and not vx_neg:  # CW
                square = [[x, y, z],
                          [xn, y, z],
                          [x, y, zn],
                          [xn, y, zn]]
            elif vx_neg and not vx_pos:  # CCW
                square = [[x, y, z],
                          [x, y, zn],
                          [xn, y, z],
                          [xn, y, zn]]
            else:  # Interior face -- can occur with certain small features
                square = None

        elif xn == x and yn > y and zn > z:
            # vert_type[x, y:yn+1, z:zn+1] = 1 # Type 1 = occupied/exterior
            vert_type[x, y+1:yn, z+1:zn] = 3 # Type 3 = interior/already included in a square

            vx_pos = False
            vx_neg = False
            if x - 1 >= 0:
                vx_neg = (voxel_model_array[x - 1, y, z] != 0)
            if x < x_len-1:
                vx_pos = (voxel_model_array[x, y, z] != 0)

            if vx_pos and not vx_neg:  # CW
                square = [[x, y, z],
                          [x, y, zn],
                          [x, yn, z],
                          [x, yn, zn]]
            elif vx_neg and not vx_pos:  # CCW
                square = [[x, y, z],
                          [x, yn, z],
                          [x, y, zn],
                          [x, yn, zn]]
            else:  # Interior face -- can occur with certain small features
                square = None

        # Add verts, tris, quads, and colors
        if square is not None:
            p = []
            for i in range(len(square)):
                new_vi = vert_index[square[i][0], square[i][1], square[i][2]]
                if new_vi == -1:
                    new_verts.append(square[i])
                    new_colors.append(vert_color[square[i][0], square[i][1], square[i][2]])
                    vert_index[square[i][0], square[i][1], square[i][2]] = vi
                    p.append(vi)
                    vi = vi + 1
                else:
                    p.append(new_vi)

            new_tris.append([p[0], p[1], p[2]])
            new_tris.append([p[3], p[2], p[1]])
            new_quads.append([p[0], p[1], p[3], p[2]])

    return vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads

Functions

def addVerticesAndTriangles(voxel_model_array: numpy.ndarray, verts_indices: numpy.ndarray, model_offsets: Tuple[], x: int, y: int, z: int, vi: int)

Find the applicable mesh vertices and triangles for a target voxel.

Args

voxel_model_array
VoxelModel.voxels
verts_indices
verts indices array
model_offsets
VoxelModel.coords
x
Target voxel X location
y
Target voxel Y location
z
Target voxel Z location
vi
Current vertex index

Returns

New verts, Updated verts indices array, New tris, Updated current vert index

Expand source code
@njit()
def addVerticesAndTriangles(voxel_model_array: np.ndarray, verts_indices: np.ndarray, model_offsets: Tuple, x: int, y: int, z: int, vi: int):
    """
    Find the applicable mesh vertices and triangles for a target voxel.

    Args:
        voxel_model_array: VoxelModel.voxels
        verts_indices: verts indices array
        model_offsets: VoxelModel.coords
        x: Target voxel X location
        y: Target voxel Y location
        z: Target voxel Z location
        vi: Current vertex index

    Returns:
        New verts, Updated verts indices array, New tris, Updated current vert index
    """
    adjacent = [
        [check_adjacent(voxel_model_array, x, y, z, 1, 0, 0), check_adjacent(voxel_model_array, x, y, z, -1, 0, 0)],
        [check_adjacent(voxel_model_array, x, y, z, 0, 1, 0), check_adjacent(voxel_model_array, x, y, z, 0, -1, 0)],
        [check_adjacent(voxel_model_array, x, y, z, 0, 0, 1), check_adjacent(voxel_model_array, x, y, z, 0, 0, -1)]
    ]

    cube_verts_indices = np.array([0, 0, 0, 0, 0, 0, 0, 0])
    verts = []
    tris = []

    if not adjacent[0][0] or not adjacent[1][0] or not adjacent[2][0]:
        vert_pos = (x+1, y+1, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[0] = verts_indices[vert_pos]

    if not adjacent[0][0] or not adjacent[1][1] or not adjacent[2][0]:
        vert_pos = (x+1, y, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[1] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][0] or not adjacent[2][0]:
        vert_pos = (x, y+1, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[2] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][1] or not adjacent[2][0]:
        vert_pos = (x, y, z+1)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[3] = verts_indices[vert_pos]

    if not adjacent[0][0] or not adjacent[1][0] or not adjacent[2][1]:
        vert_pos = (x+1, y+1, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[4] = verts_indices[vert_pos]

    if not adjacent[0][0] or not adjacent[1][1] or not adjacent[2][1]:
        vert_pos = (x+1, y, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[5] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][0] or not adjacent[2][1]:
        vert_pos = (x, y+1, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[6] = verts_indices[vert_pos]

    if not adjacent[0][1] or not adjacent[1][1] or not adjacent[2][1]:
        vert_pos = (x, y, z)
        if verts_indices[vert_pos] < 1:
            verts_indices[vert_pos] = vi
            verts.append([vert_pos[0]+model_offsets[0], vert_pos[1]+model_offsets[1], vert_pos[2]+model_offsets[2]])
            vi = vi + 1
        cube_verts_indices[7] = verts_indices[vert_pos]

    if not adjacent[0][0]:
        tris.append([cube_verts_indices[0] - 1, cube_verts_indices[1] - 1, cube_verts_indices[5] - 1])
        tris.append([cube_verts_indices[5] - 1, cube_verts_indices[4] - 1, cube_verts_indices[0] - 1])

    if not adjacent[1][0]:
        tris.append([cube_verts_indices[2] - 1, cube_verts_indices[0] - 1, cube_verts_indices[4] - 1])
        tris.append([cube_verts_indices[4] - 1, cube_verts_indices[6] - 1, cube_verts_indices[2] - 1])

    if not adjacent[2][0]:
        tris.append([cube_verts_indices[1] - 1, cube_verts_indices[0] - 1, cube_verts_indices[2] - 1])
        tris.append([cube_verts_indices[2] - 1, cube_verts_indices[3] - 1, cube_verts_indices[1] - 1])

    if not adjacent[0][1]:
        tris.append([cube_verts_indices[3] - 1, cube_verts_indices[2] - 1, cube_verts_indices[6] - 1])
        tris.append([cube_verts_indices[6] - 1, cube_verts_indices[7] - 1, cube_verts_indices[3] - 1])

    if not adjacent[1][1]:
        tris.append([cube_verts_indices[1] - 1, cube_verts_indices[3] - 1, cube_verts_indices[7] - 1])
        tris.append([cube_verts_indices[7] - 1, cube_verts_indices[5] - 1, cube_verts_indices[1] - 1])

    if not adjacent[2][1]:
        tris.append([cube_verts_indices[4] - 1, cube_verts_indices[5] - 1, cube_verts_indices[7] - 1])
        tris.append([cube_verts_indices[7] - 1, cube_verts_indices[6] - 1, cube_verts_indices[4] - 1])

    return verts, verts_indices.astype(np.uint32), tris, vi
def check_adjacent(input_model: numpy.ndarray, x_coord: int, y_coord: int, z_coord: int, x_dir: int, y_dir: int, z_dir: int)

Check if a target voxel has another voxel adjacent to it in the specified direction.

Args

input_model
VoxelModel.voxels
x_coord
Target voxel X location
y_coord
Target voxel Y location
z_coord
Target voxel Z location
x_dir
Specify X direction and distance (usually 1 or -1)
y_dir
Specify Y direction and distance (usually 1 or -1)
z_dir
Specify Z direction and distance (usually 1 or -1)

Returns

Adjacent voxel present/not present

Expand source code
@njit()
def check_adjacent(input_model: np.ndarray, x_coord: int, y_coord: int, z_coord: int, x_dir: int, y_dir: int, z_dir: int):
    """
    Check if a target voxel has another voxel adjacent to it in the specified direction.

    Args:
        input_model: VoxelModel.voxels
        x_coord: Target voxel X location
        y_coord: Target voxel Y location
        z_coord: Target voxel Z location
        x_dir: Specify X direction and distance (usually 1 or -1)
        y_dir: Specify Y direction and distance (usually 1 or -1)
        z_dir: Specify Z direction and distance (usually 1 or -1)
    
    Returns:
        Adjacent voxel present/not present
    """
    x_coord_new = x_coord+x_dir
    y_coord_new = y_coord+y_dir
    z_coord_new = z_coord+z_dir

    x_in_bounds = (x_coord_new >= 0) and (x_coord_new < input_model.shape[0])
    y_in_bounds = (y_coord_new >= 0) and (y_coord_new < input_model.shape[1])
    z_in_bounds = (z_coord_new >= 0) and (z_coord_new < input_model.shape[2])

    if x_in_bounds and y_in_bounds and z_in_bounds and input_model[x_coord_new, y_coord_new, z_coord_new] > 0:
        return True
    else:
        return False
def findSquare(vi: int, vert_type: numpy.ndarray, vert_index: numpy.ndarray, vert_color: numpy.ndarray, voxel_model_array: numpy.ndarray, x: int, y: int, z: int, dx: int, dy: int, dz: int)

Find the largest square starting from a given point and generate the corresponding points and tris.

Args

vi
Current vertex index
vert_type
Array of vertex types
vert_index
Array of vertex indices
vert_color
Array of vertex colors
voxel_model_array
Voxel data array
x
Target voxel X
y
Target voxel Y
z
Target voxel Z
dx
Square search step in X
dy
Square search step in Y
dz
Square search step in Z

Returns

vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads

Expand source code
@njit()
def findSquare(vi: int, vert_type: np.ndarray, vert_index: np.ndarray, vert_color: np.ndarray, voxel_model_array: np.ndarray, x: int, y: int, z: int, dx: int, dy: int, dz: int):
    """
    Find the largest square starting from a given point and generate the corresponding points and tris.

    Args:
        vi: Current vertex index
        vert_type: Array of vertex types
        vert_index: Array of vertex indices
        vert_color: Array of vertex colors
        voxel_model_array: Voxel data array
        x: Target voxel X
        y: Target voxel Y
        z: Target voxel Z
        dx: Square search step in X
        dy: Square search step in Y
        dz: Square search step in Z

    Returns:
        vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads
    """
    x_len, y_len, z_len = vert_type.shape
    new_verts = []
    new_colors = []
    new_tris = []
    new_quads = []

    vert_on_surface = (vert_type[x, y, z] == 1 or vert_type[x, y, z] == 2)
    if vert_on_surface and x+dx < x_len and y+dy < y_len and z+dz < z_len:  # Point is a face vertex and next point is in bounds
        xn = x
        yn = y
        zn = z

        for i in range(1, max(x_len - x, y_len - y, z_len - z)):  # See if a square can be found starting at this point
            xn = x + dx * i
            yn = y + dy * i
            zn = z + dz * i

            # Check if endpoint is in bounds
            in_range = [xn < x_len,
                        yn < y_len,
                        zn < z_len]

            if not np.all(np.array(in_range)):
                xn = x + dx * (i-1)
                yn = y + dy * (i-1)
                zn = z + dz * (i-1)
                break

            face = (vert_type[x:xn+1, y:yn+1,  z:zn+1] == 1)
            blocking = (vert_type[x:xn+1, y:yn+1,  z:zn+1] == 2)
            on_surface = np.logical_or(face, blocking)

            # Check if square includes only surface vertices
            if not np.all(on_surface):
                xn = x + dx * (i-1)
                yn = y + dy * (i-1)
                zn = z + dz * (i-1)
                break

            # Check if square includes any blocking vertices
            if np.any(blocking):
                break

        square = None

        # Determine vert coords based on search direction
        if xn > x and yn > y and zn == z:
            # vert_type[x:xn+1, y:yn+1, z] = 1 # Type 1 = occupied/exterior
            vert_type[x+1:xn, y+1:yn, z] = 3 # Type 3 = interior/already included in a square

            vx_pos = False
            vx_neg = False
            if z - 1 >= 0:
                vx_neg = (voxel_model_array[x, y, z - 1] != 0)
            if z < z_len-1:
                vx_pos = (voxel_model_array[x, y, z] != 0)

            if vx_pos and not vx_neg: # CW
                square = [[x, y, z],
                          [x, yn, z],
                          [xn, y, z],
                          [xn, yn, z]]
            elif vx_neg and not vx_pos: # CCW
                square = [[x, y, z],
                          [xn, y, z],
                          [x, yn, z],
                          [xn, yn, z]]
            else: # Interior face -- can occur with certain small features
                square = None

        elif xn > x and yn == y and zn > z:
            # vert_type[x:xn+1, y, z:zn+1] = 1 # Type 1 = occupied/exterior
            vert_type[x+1:xn, y, z+1:zn] = 3 # Type 3 = interior/already included in a square

            vx_pos = False
            vx_neg = False
            if y - 1 >= 0:
                vx_neg = (voxel_model_array[x, y - 1, z] != 0)
            if y < y_len-1:
                vx_pos = (voxel_model_array[x, y, z] != 0)

            if vx_pos and not vx_neg:  # CW
                square = [[x, y, z],
                          [xn, y, z],
                          [x, y, zn],
                          [xn, y, zn]]
            elif vx_neg and not vx_pos:  # CCW
                square = [[x, y, z],
                          [x, y, zn],
                          [xn, y, z],
                          [xn, y, zn]]
            else:  # Interior face -- can occur with certain small features
                square = None

        elif xn == x and yn > y and zn > z:
            # vert_type[x, y:yn+1, z:zn+1] = 1 # Type 1 = occupied/exterior
            vert_type[x, y+1:yn, z+1:zn] = 3 # Type 3 = interior/already included in a square

            vx_pos = False
            vx_neg = False
            if x - 1 >= 0:
                vx_neg = (voxel_model_array[x - 1, y, z] != 0)
            if x < x_len-1:
                vx_pos = (voxel_model_array[x, y, z] != 0)

            if vx_pos and not vx_neg:  # CW
                square = [[x, y, z],
                          [x, y, zn],
                          [x, yn, z],
                          [x, yn, zn]]
            elif vx_neg and not vx_pos:  # CCW
                square = [[x, y, z],
                          [x, yn, z],
                          [x, y, zn],
                          [x, yn, zn]]
            else:  # Interior face -- can occur with certain small features
                square = None

        # Add verts, tris, quads, and colors
        if square is not None:
            p = []
            for i in range(len(square)):
                new_vi = vert_index[square[i][0], square[i][1], square[i][2]]
                if new_vi == -1:
                    new_verts.append(square[i])
                    new_colors.append(vert_color[square[i][0], square[i][1], square[i][2]])
                    vert_index[square[i][0], square[i][1], square[i][2]] = vi
                    p.append(vi)
                    vi = vi + 1
                else:
                    p.append(new_vi)

            new_tris.append([p[0], p[1], p[2]])
            new_tris.append([p[3], p[2], p[1]])
            new_quads.append([p[0], p[1], p[3], p[2]])

    return vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads
def generateColors(n: int, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1))

Generate a colors list with the given number of elements

Args

n
Number of vertices in target model
color
Mesh color in the format (r, g, b, a)

Returns

List of vertex colors

Expand source code
def generateColors(n: int, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
    """
    Generate a colors list with the given number of elements

    Args:
        n: Number of vertices in target model
        color: Mesh color in the format (r, g, b, a)
    
    Returns:
        List of vertex colors
    """
    verts_colors = []
    voxel_color = list(color)
    for i in range(n):
        verts_colors.append(voxel_color)
    verts_colors = np.array(verts_colors)
    return verts_colors
def markInsideCorner(vert_type, x, y, z)

Determine if target voxel is an inside corner.

Args

vert_type
Array of vertex types
x
Target voxel X
y
Target voxel Y
z
Target voxel Z

Returns

Updated array of vertex types

Expand source code
@njit()
def markInsideCorner(vert_type, x, y, z):
    """
    Determine if target voxel is an inside corner.

    Args:
        vert_type: Array of vertex types
        x: Target voxel X
        y: Target voxel Y
        z: Target voxel Z

    Returns:
        Updated array of vertex types
    """
    x_len, y_len, z_len = vert_type.shape
    adjacent_empty = [True, True, True, True, True, True]

    if x > 0:
        adjacent_empty[0] = (vert_type[x - 1, y, z] == 0)
    if x < x_len-1:
        adjacent_empty[1] = (vert_type[x + 1, y, z] == 0)

    if y > 0:
        adjacent_empty[2] = (vert_type[x, y - 1, z] == 0)
    if y < y_len-1:
        adjacent_empty[3] = (vert_type[x, y + 1, z] == 0)

    if z > 0:
        adjacent_empty[4] = (vert_type[x, y, z - 1] == 0)
    if z < z_len-1:
        adjacent_empty[5] = (vert_type[x, y, z + 1] == 0)

    is_face = [adjacent_empty[0] != adjacent_empty[1],
               adjacent_empty[2] != adjacent_empty[3],
               adjacent_empty[4] != adjacent_empty[5]]

    # Inside corner  - 0 faces
    # Face           - 1 face
    # Outside edge   - 2 faces
    # Outside corner - 3 faces
    if vert_type[x, y, z] == 1 and np.sum(np.array(is_face)) == 0:
        vert_type[x, y, z] = 2  # Type 2 = blocking

    return vert_type
def markInterior(vert_type: numpy.ndarray, x: int, y: int, z: int)

Determine if target voxel is an interior voxel.

Args

vert_type
Array of vertex types
x
Target voxel X
y
Target voxel Y
z
Target voxel Z

Returns

Updated array of vertex types

Expand source code
@njit()
def markInterior(vert_type: np.ndarray, x: int, y: int, z: int):
    """
    Determine if target voxel is an interior voxel.

    Args:
        vert_type: Array of vertex types
        x: Target voxel X
        y: Target voxel Y
        z: Target voxel Z

    Returns:
        Updated array of vertex types
    """
    if np.all(vert_type[x-1:x+2, y-1:y+2, z-1:z+2] != 0):
        vert_type[x, y, z] = 3 # Type 3 = interior/already included in a square
    return vert_type

Classes

class Mesh (voxels: Optional[numpy.ndarray], verts: numpy.ndarray, verts_colors: numpy.ndarray, tris: numpy.ndarray, resolution: float)

Mesh object that can be exported or passed to a Plot object.

Initialize a Mesh object.

Args

voxels
Voxel data array
verts
List of coordinates of surface vertices
verts_colors
List of colors associated with each vertex
tris
List of the sets of vertices associated with triangular faces
resolution
Number of voxels per mm
Expand source code
class Mesh:
    """
    Mesh object that can be exported or passed to a Plot object.
    """

    def __init__(self, voxels: TypeUnion[np.ndarray, None], verts: np.ndarray, verts_colors: np.ndarray, tris: np.ndarray, resolution: float):
        """
        Initialize a Mesh object.

        Args:
            voxels: Voxel data array
            verts: List of coordinates of surface vertices
            verts_colors: List of colors associated with each vertex
            tris: List of the sets of vertices associated with triangular faces
            resolution: Number of voxels per mm
        """
        if voxels is not None:
            self.model = voxels
        else:
            self.model = np.array([[[0]]])

        self.verts = verts
        self.colors = verts_colors
        self.tris = tris
        self.res = resolution

    @classmethod
    def fromMeshFile(cls, filename: str, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
        """
        Import a mesh file to a mesh object.

        ----

        Example:

        ``mesh1 = vf.Mesh.fromMeshFile(example.stl)``

        ----

        Args:
            filename: File name with extension
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            Mesh
        """
        # Open file
        data = meshio.read(filename)

        # Read verts
        verts = np.array(data.points)

        # Align to origin
        x_min = np.min(verts[:, 0])
        y_min = np.min(verts[:, 1])
        z_min = np.min(verts[:, 2])
        verts[:, 0] = np.subtract(verts[:, 0], x_min)
        verts[:, 1] = np.subtract(verts[:, 1], y_min)
        verts[:, 2] = np.subtract(verts[:, 2], z_min)

        # Generate colors
        verts_colors = generateColors(len(verts), color)

        # Read tris
        tris = []
        for cell in data.cells:
            if cell[0] == 'triangle':
                for tri in cell[1]:
                    tris.append(tri)
        tris = np.array(tris)

        return cls(None, verts, verts_colors, tris, 1)

    @classmethod
    def fromVoxelModel(cls, voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None):
        """
        Generate a mesh object from a VoxelModel object.

        ----

        Example:

        ``mesh1 = vf.Mesh.fromVoxelModel(model1)``

        ----

        Args:
            voxel_model: VoxelModel object to be converted to a mesh
            color: Mesh color in the format (r, g, b, a), None to use voxel colors
        
        Returns:
            Mesh
        """
        voxel_model_fit = voxel_model.fitWorkspace()
        voxel_model_array = voxel_model_fit.voxels.astype(np.uint16)
        model_materials = voxel_model_fit.materials
        model_offsets = voxel_model_fit.coords

        # Find exterior voxels
        exterior_voxels_array = voxel_model_fit.difference(voxel_model_fit.erode(radius=1, connectivity=1)).voxels
        
        x_len, y_len, z_len = voxel_model_array.shape
        
        # Create list of exterior voxel coordinates
        exterior_voxels_coords = []
        for x in tqdm(range(x_len), desc='Finding exterior voxels'):
            for y in range(y_len):
                for z in range(z_len):
                    if exterior_voxels_array[x, y, z] != 0:
                        exterior_voxels_coords.append([x, y, z])

        # Get voxel array
        voxel_model_array[voxel_model_array < 0] = 0

        # Initialize arrays
        verts = []
        verts_colors = []
        verts_indices = np.zeros((x_len+1, y_len+1, z_len+1))
        tris = []
        vi = 1  # Tracks current vertex index

        # Loop through voxel_model_array data
        for voxel_coords in tqdm(exterior_voxels_coords, desc='Meshing'):
            x, y, z = voxel_coords

            if color is None:
                r = 0
                g = 0
                b = 0

                for i in range(voxel_model.materials.shape[1]-1):
                    r = r + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['r']
                    g = g + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['g']
                    b = b + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['b']

                r = 1 if r > 1 else r
                g = 1 if g > 1 else g
                b = 1 if b > 1 else b

                a = 1 - model_materials[voxel_model_array[x, y, z]][1]

                voxel_color = [r, g, b, a]
            else:
                voxel_color = list(color)

            # Add cube vertices
            new_verts, verts_indices, new_tris, vi = addVerticesAndTriangles(voxel_model_array, verts_indices, model_offsets, x, y, z, vi)
            verts += new_verts
            tris += new_tris

            # Apply color to all vertices
            for i in range(len(new_verts)):
                verts_colors.append(voxel_color)

        verts = np.array(verts, dtype=np.float32)
        verts_colors = np.array(verts_colors, dtype=np.float32)
        tris = np.array(tris, dtype=np.uint32)

        return cls(voxel_model_array, verts, verts_colors, tris, voxel_model.resolution)

    @classmethod
    def simpleSquares(cls, voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None):
        """
        Generate a mesh object from a VoxelModel object using large square faces.

        This function can greatly reduce the file size of generated meshes. However, it may not correctly recognize
        small (1 voxel) model features and currently produces files with a nonstandard vertex arrangement. Use at your own
        risk.

        ----

        Example:

        ``mesh1 = vf.Mesh.simpleSquares(model1)``

        ----

        Args:
            voxel_model: VoxelModel object to be converted to a mesh
            color: Mesh color in the format (r, g, b, a), None to use voxel colors
        
        Returns:
            Mesh
        """
        voxel_model_fit = voxel_model.fitWorkspace()
        voxel_model_array = voxel_model_fit.voxels.astype(np.uint16)
        model_materials = voxel_model_fit.materials
        model_offsets = voxel_model_fit.coords

        x_len, y_len, z_len = voxel_model_array.shape

        # Determine vertex types
        vert_type = np.zeros((x_len + 1, y_len + 1, z_len + 1), dtype=np.uint8)
        vert_color = np.zeros((x_len + 1, y_len + 1, z_len + 1, 4), dtype=np.float32)
        for x in tqdm(range(x_len), desc='Finding voxel vertices'):
            for y in range(y_len):
                for z in range(z_len):
                    if voxel_model_array[x, y, z] > 0:
                        vert_type[x:x+2, y:y+2, z:z+2] = 1 # Type 1 = occupied/exterior

                        if color is None:
                            r = 0
                            g = 0
                            b = 0

                            for i in range(voxel_model.materials.shape[1] - 1):
                                r = r + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['r']
                                g = g + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['g']
                                b = b + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['b']

                            r = 1 if r > 1 else r
                            g = 1 if g > 1 else g
                            b = 1 if b > 1 else b

                            a = 1 - model_materials[voxel_model_array[x, y, z]][1]

                            voxel_color = np.array([r, g, b, a])
                        else:
                            voxel_color = np.array(color)

                        for cx in range(x, x+2):
                            for cy in range(y, y+2):
                                for cz in range(z, z+2):
                                    vert_color[cx, cy, cz, :] = voxel_color

        for x in tqdm(range(1, x_len), desc='Finding interior vertices'):
            for y in range(1, y_len):
                for z in range(1, z_len):
                    vert_type = markInterior(vert_type, x, y, z)

        for x in tqdm(range(0, x_len+1), desc='Finding feature vertices'):
            for y in range(0, y_len+1):
                for z in range(0, z_len+1):
                    vert_type = markInsideCorner(vert_type, x, y, z)

        # Initialize arrays
        vi = 0 # Tracks current vertex index
        verts = []
        colors = []
        tris = []
        quads = []
        vert_index = np.multiply(np.ones_like(vert_type, dtype=np.int32), -1)

        for x in tqdm(range(x_len + 1), desc='Meshing'):
            for y in range(y_len + 1):
                for z in range(z_len + 1):
                    dirs = [[1, 1, 0], [1, 0, 1], [0, 1, 1]]
                    for d in dirs:
                        vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads = findSquare(vi, vert_type, vert_index, vert_color, voxel_model_array, x, y, z, d[0], d[1], d[2])
                        verts += new_verts
                        colors += new_colors
                        tris += new_tris
                        quads += new_quads

        verts = np.array(verts, dtype=np.float32)
        colors = np.array(colors, dtype=np.float32)
        tris = np.array(tris, dtype=np.uint32)
        quads = np.array(quads, dtype=np.uint32)

        # Shift model to align with origin
        verts[:, 0] = np.add(verts[:, 0], model_offsets[0])
        verts[:, 1] = np.add(verts[:, 1], model_offsets[1])
        verts[:, 2] = np.add(verts[:, 2], model_offsets[2])

        return cls(voxel_model_array, verts, colors, tris, voxel_model.resolution)

    @classmethod
    def marchingCubes(cls, voxel_model: VoxelModel, smooth: bool = False, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
        """
        Generate a mesh object from a VoxelModel object using a marching cubes algorithm.

        This meshing approach is best suited to high resolution models where some smoothing is acceptable.

        Args:
            voxel_model: VoxelModel object to be converted to a mesh
            smooth: Enable smoothing
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            None
        """
        voxel_model_fit = voxel_model.fitWorkspace().getOccupied()
        voxels = voxel_model_fit.voxels.astype(np.uint16)
        x, y, z = voxels.shape
        model_offsets = voxel_model_fit.coords

        voxels_padded = np.zeros((x + 2, y + 2, z + 2))
        voxels_padded[1:-1, 1:-1, 1:-1] = voxels

        if smooth:
            voxels_padded = mcubes.smooth(voxels_padded)
            levelset = 0
        else:
            levelset = 0.5

        verts, tris = mcubes.marching_cubes(voxels_padded, levelset)

        # Shift model to align with origin
        verts = np.subtract(verts, 0.5)
        verts[:, 0] = np.add(verts[:, 0], model_offsets[0])
        verts[:, 1] = np.add(verts[:, 1], model_offsets[1])
        verts[:, 2] = np.add(verts[:, 2], model_offsets[2])

        verts_colors = generateColors(len(verts), color)

        return cls(voxels_padded, verts, verts_colors, tris, voxel_model.resolution)

    @classmethod
    def copy(cls, mesh):
        """
        Initialize a Mesh that is a copy of another mesh.

        Args:
            mesh: Reference Mesh object
        
        Returns:
            Mesh
        """
        new_mesh = cls(np.copy(mesh.model), np.copy(mesh.verts), np.copy(mesh.colors), np.copy(mesh.tris), mesh.res)
        return new_mesh

    def setResolution(self, resolution: float):
        """
        Change the defined resolution of a mesh.

        The mesh resolution will determine the scale of plots and exported mesh files.

        Args:
            resolution: Number of voxels per mm (higher number = finer resolution)
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.res = resolution
        return new_mesh

    def scale(self, factor: float):
        """
        Apply a scaling factor to a mesh.

        Args:
            factor: Scaling factor
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.verts = np.multiply(self.verts, factor)
        return new_mesh

    def simplify(self, percent_verts: float, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
        """
        Simplify a mesh to contain a given percentage of the original number of vertices.

        More information on the simplification algorithm is available at: https://github.com/jannessm/quadric-mesh-simplification

        Args:
            percent_verts: Percentage of vertex count allowed in the result mesh, 0-1
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            Mesh
        """
        num_verts = self.verts.shape[0]
        target_verts = num_verts * percent_verts

        new_verts, new_tris = simplify_mesh(positions=self.verts.astype(np.double), face=self.tris.astype(np.uint32), num_nodes=target_verts)
        verts_colors = generateColors(len(new_verts), color)

        return Mesh(np.copy(self.model), new_verts, verts_colors, new_tris, self.res)

    def translate(self, vector: Tuple[float, float, float]):
        """
        Translate a model by the specified vector.

        Args:
            vector: Translation vector in voxels
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.verts[:, 0] = np.add(self.verts[:, 0], vector[0])
        new_mesh.verts[:, 1] = np.add(self.verts[:, 1], vector[1])
        new_mesh.verts[:, 2] = np.add(self.verts[:, 2], vector[2])
        return new_mesh

    def translateMM(self, vector: Tuple[float, float, float]):
        """
        Translate a model by the specified vector.

        Args:
            vector: Translation vector in mm
        
        Returns:
            Mesh
        """
        xV = vector[0] * self.res
        yV = vector[1] * self.res
        zV = vector[2] * self.res
        new_mesh = self.translate((xV, yV, zV))
        return new_mesh

    def setColor(self, color: Tuple[float, float, float, float]):
        """
        Change the color of a mesh.

        Args:
            color: Mesh color in the format (r, g, b, a)
        
        Returns:
            Mesh
        """
        new_mesh = Mesh.copy(self)
        new_mesh.colors = generateColors(len(self.verts), color)
        return new_mesh

    def plot(self, plot = None, name: str = 'mesh', wireframe: bool = True, mm_scale: bool = False, **kwargs):
        """
        Add mesh to a K3D plot in Jupyter Notebook.

        Additional display options:

        - flat_shading: `bool`. Whether mesh should display with flat shading.
        - opacity: `float`. Opacity of mesh.
        - volume: `array_like`. 3D array of `float`
        - volume_bounds: `array_like`. 6-element tuple specifying the bounds of the volume data (x0, x1, y0, y1, z0, z1)
        - opacity_function: `array`. A list of float tuples (attribute value, opacity), sorted by attribute value. The first tuples should have value 0.0, the last 1.0; opacity is in the range 0.0 to 1.0.
        - side: `string`. Control over which side to render for a mesh. Legal values are `front`, `back`, `double`.
        - texture: `bytes`. Image data in a specific format.
        - texture_file_format: `str`. Format of the data, it should be the second part of MIME format of type 'image/', for example 'jpeg', 'png', 'gif', 'tiff'.
        - uvs: `array_like`. Array of float uvs for the texturing, coresponding to each vertex.
        - kwargs: `dict`. Dictionary arguments to configure transform and model_matrix.

        More information available at: https://github.com/K3D-tools/K3D-jupyter

        Args:
            plot: Plot object to add mesh to
            name: Mesh name
            wireframe: Enable displaying mesh as a wireframe
            mm_scale: Enable to use a mm plot scale, disable to use a voxel plot scale
            kwargs: Additional display options (see above)
        
        Returns:
            K3D plot object
        """
        # Get verts
        verts = self.verts

        # Adjust coordinate scale
        if mm_scale:
            verts = np.divide(verts, self.res)

        # Get tris
        tris = self.tris

        # Get colors
        colors = []
        for c in self.colors:
            colors.append(rgb_to_hex(c[0], c[1], c[2]))
        colors = np.array(colors, dtype=np.uint32)

        # Plot
        if plot is None:
            plot = k3d.plot()

        plot += k3d.mesh(verts.astype(np.float32), tris.astype(np.uint32), colors=colors, name=name, wireframe=wireframe, **kwargs)
        return plot

    def viewer(self, grids: bool = False, drawEdges: bool = True,
               edgeColor: Tuple[float, float, float, float] = (0, 0, 0, 0.5),
               positionOffset: Tuple[int, int, int] = (0, 0, 0), viewAngle: Tuple[int, int, int] = (40, 30, 300),
               resolution: Tuple[int, int] = (1280, 720), name: str = 'Plot 1', export: bool = False):
        """
        Display the mesh in a 3D viewer window.

        This function will block program execution until viewer window is closed

        Args:
            grids: Enable/disable display of XYZ axes and grids
            drawEdges: Enable/disable display of voxel edges
            edgeColor: Set display color of voxel edges
            positionOffset: Offset of the camera target from the center of the model in voxels
            viewAngle: Elevation, Azimuth, and Distance of the camera
            resolution: Window resolution in px
            name: Plot window name
            export: Enable/disable exporting a screenshot of the plot
        
        Returns:
            None
        """
        app = qtw.QApplication(sys.argv)

        mesh_data = pgo.MeshData(vertexes=self.verts, faces=self.tris, vertexColors=self.colors, faceColors=None)
        mesh_item = pgo.GLMeshItem(meshdata=mesh_data, shader='balloon', drawEdges=drawEdges, edgeColor=edgeColor,
                                   smooth=False, computeNormals=False, glOptions='translucent')

        widget = pgo.GLViewWidget()
        widget.setBackgroundColor('w')
        widget.addItem(mesh_item)

        if grids:
            # Add grids
            gx = pgo.GLGridItem()
            gx.setSize(x=50, y=50, z=50)
            gx.rotate(90, 0, 1, 0)
            gx.translate(-0.5, 24.5, 24.5)
            widget.addItem(gx)
            gy = pgo.GLGridItem()
            gy.setSize(x=50, y=50, z=50)
            gy.rotate(90, 1, 0, 0)
            gy.translate(24.5, -0.5, 24.5)
            widget.addItem(gy)
            gz = pgo.GLGridItem()
            gz.setSize(x=50, y=50, z=50)
            gz.translate(24.5, 24.5, -0.5)
            widget.addItem(gz)

            # Add axes
            ptsx = np.array([[-0.5, -0.5, -0.5], [50, -0.5, -0.5]])
            pltx = pgo.GLLinePlotItem(pos=ptsx, color=(1, 0, 0, 1), width=1, antialias=True)
            widget.addItem(pltx)
            ptsy = np.array([[-0.5, -0.5, -0.5], [-0.5, 50, -0.5]])
            plty = pgo.GLLinePlotItem(pos=ptsy, color=(0, 1, 0, 1), width=1, antialias=True)
            widget.addItem(plty)
            ptsz = np.array([[-0.5, -0.5, -0.5], [-0.5, -0.5, 50]])
            pltz = pgo.GLLinePlotItem(pos=ptsz, color=(0, 0, 1, 1), width=1, antialias=True)
            widget.addItem(pltz)

        # Set plot options
        widget.opts['center'] = qg.QVector3D(((self.model.shape[0] / self.res) / 2) + positionOffset[0],
                                             ((self.model.shape[1] / self.res) / 2) + positionOffset[1],
                                             ((self.model.shape[2] / self.res) / 2) + positionOffset[2])
        widget.opts['elevation'] = viewAngle[0]
        widget.opts['azimuth'] = viewAngle[1]
        widget.opts['distance'] = viewAngle[2]
        widget.resize(resolution[0], resolution[1])

        # Show plot
        widget.setWindowTitle(str(name))
        widget.show()

        app.processEvents()

        # if export: # TODO: Fix export code
        #     widget.paintGL()
        #     widget.grabFrameBuffer().save(str(name) + '.png')

        print('Close viewer to resume program')
        app.exec_()
        app.quit()

    # Export model from mesh data
    def export(self, filename: str):
        """
        Save a copy of the mesh with the specified name and file format.

        ----

        Example:

        ``mesh1.export('result.stl')``

        ----

        Args:
            filename: File name with extension
        
        Returns:
            None
        """
        # Adjust coordinate scale
        verts = np.divide(self.verts, self.res)

        cells = {
            "triangle": self.tris
        }

        output_mesh = meshio.Mesh(verts, cells)
        meshio.write(filename, output_mesh)

Static methods

def copy(mesh)

Initialize a Mesh that is a copy of another mesh.

Args

mesh
Reference Mesh object

Returns

Mesh

Expand source code
@classmethod
def copy(cls, mesh):
    """
    Initialize a Mesh that is a copy of another mesh.

    Args:
        mesh: Reference Mesh object
    
    Returns:
        Mesh
    """
    new_mesh = cls(np.copy(mesh.model), np.copy(mesh.verts), np.copy(mesh.colors), np.copy(mesh.tris), mesh.res)
    return new_mesh
def fromMeshFile(filename: str, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1))

Import a mesh file to a mesh object.


Example:

mesh1 = vf.Mesh.fromMeshFile(example.stl)


Args

filename
File name with extension
color
Mesh color in the format (r, g, b, a)

Returns

Mesh

Expand source code
@classmethod
def fromMeshFile(cls, filename: str, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
    """
    Import a mesh file to a mesh object.

    ----

    Example:

    ``mesh1 = vf.Mesh.fromMeshFile(example.stl)``

    ----

    Args:
        filename: File name with extension
        color: Mesh color in the format (r, g, b, a)
    
    Returns:
        Mesh
    """
    # Open file
    data = meshio.read(filename)

    # Read verts
    verts = np.array(data.points)

    # Align to origin
    x_min = np.min(verts[:, 0])
    y_min = np.min(verts[:, 1])
    z_min = np.min(verts[:, 2])
    verts[:, 0] = np.subtract(verts[:, 0], x_min)
    verts[:, 1] = np.subtract(verts[:, 1], y_min)
    verts[:, 2] = np.subtract(verts[:, 2], z_min)

    # Generate colors
    verts_colors = generateColors(len(verts), color)

    # Read tris
    tris = []
    for cell in data.cells:
        if cell[0] == 'triangle':
            for tri in cell[1]:
                tris.append(tri)
    tris = np.array(tris)

    return cls(None, verts, verts_colors, tris, 1)
def fromVoxelModel(voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None)

Generate a mesh object from a VoxelModel object.


Example:

mesh1 = vf.Mesh.fromVoxelModel(model1)


Args

voxel_model
VoxelModel object to be converted to a mesh
color
Mesh color in the format (r, g, b, a), None to use voxel colors

Returns

Mesh

Expand source code
@classmethod
def fromVoxelModel(cls, voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None):
    """
    Generate a mesh object from a VoxelModel object.

    ----

    Example:

    ``mesh1 = vf.Mesh.fromVoxelModel(model1)``

    ----

    Args:
        voxel_model: VoxelModel object to be converted to a mesh
        color: Mesh color in the format (r, g, b, a), None to use voxel colors
    
    Returns:
        Mesh
    """
    voxel_model_fit = voxel_model.fitWorkspace()
    voxel_model_array = voxel_model_fit.voxels.astype(np.uint16)
    model_materials = voxel_model_fit.materials
    model_offsets = voxel_model_fit.coords

    # Find exterior voxels
    exterior_voxels_array = voxel_model_fit.difference(voxel_model_fit.erode(radius=1, connectivity=1)).voxels
    
    x_len, y_len, z_len = voxel_model_array.shape
    
    # Create list of exterior voxel coordinates
    exterior_voxels_coords = []
    for x in tqdm(range(x_len), desc='Finding exterior voxels'):
        for y in range(y_len):
            for z in range(z_len):
                if exterior_voxels_array[x, y, z] != 0:
                    exterior_voxels_coords.append([x, y, z])

    # Get voxel array
    voxel_model_array[voxel_model_array < 0] = 0

    # Initialize arrays
    verts = []
    verts_colors = []
    verts_indices = np.zeros((x_len+1, y_len+1, z_len+1))
    tris = []
    vi = 1  # Tracks current vertex index

    # Loop through voxel_model_array data
    for voxel_coords in tqdm(exterior_voxels_coords, desc='Meshing'):
        x, y, z = voxel_coords

        if color is None:
            r = 0
            g = 0
            b = 0

            for i in range(voxel_model.materials.shape[1]-1):
                r = r + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['r']
                g = g + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['g']
                b = b + model_materials[voxel_model_array[x, y, z]][i+1] * material_properties[i]['b']

            r = 1 if r > 1 else r
            g = 1 if g > 1 else g
            b = 1 if b > 1 else b

            a = 1 - model_materials[voxel_model_array[x, y, z]][1]

            voxel_color = [r, g, b, a]
        else:
            voxel_color = list(color)

        # Add cube vertices
        new_verts, verts_indices, new_tris, vi = addVerticesAndTriangles(voxel_model_array, verts_indices, model_offsets, x, y, z, vi)
        verts += new_verts
        tris += new_tris

        # Apply color to all vertices
        for i in range(len(new_verts)):
            verts_colors.append(voxel_color)

    verts = np.array(verts, dtype=np.float32)
    verts_colors = np.array(verts_colors, dtype=np.float32)
    tris = np.array(tris, dtype=np.uint32)

    return cls(voxel_model_array, verts, verts_colors, tris, voxel_model.resolution)
def marchingCubes(voxel_model: VoxelModel, smooth: bool = False, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1))

Generate a mesh object from a VoxelModel object using a marching cubes algorithm.

This meshing approach is best suited to high resolution models where some smoothing is acceptable.

Args

voxel_model
VoxelModel object to be converted to a mesh
smooth
Enable smoothing
color
Mesh color in the format (r, g, b, a)

Returns

None

Expand source code
@classmethod
def marchingCubes(cls, voxel_model: VoxelModel, smooth: bool = False, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
    """
    Generate a mesh object from a VoxelModel object using a marching cubes algorithm.

    This meshing approach is best suited to high resolution models where some smoothing is acceptable.

    Args:
        voxel_model: VoxelModel object to be converted to a mesh
        smooth: Enable smoothing
        color: Mesh color in the format (r, g, b, a)
    
    Returns:
        None
    """
    voxel_model_fit = voxel_model.fitWorkspace().getOccupied()
    voxels = voxel_model_fit.voxels.astype(np.uint16)
    x, y, z = voxels.shape
    model_offsets = voxel_model_fit.coords

    voxels_padded = np.zeros((x + 2, y + 2, z + 2))
    voxels_padded[1:-1, 1:-1, 1:-1] = voxels

    if smooth:
        voxels_padded = mcubes.smooth(voxels_padded)
        levelset = 0
    else:
        levelset = 0.5

    verts, tris = mcubes.marching_cubes(voxels_padded, levelset)

    # Shift model to align with origin
    verts = np.subtract(verts, 0.5)
    verts[:, 0] = np.add(verts[:, 0], model_offsets[0])
    verts[:, 1] = np.add(verts[:, 1], model_offsets[1])
    verts[:, 2] = np.add(verts[:, 2], model_offsets[2])

    verts_colors = generateColors(len(verts), color)

    return cls(voxels_padded, verts, verts_colors, tris, voxel_model.resolution)
def simpleSquares(voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None)

Generate a mesh object from a VoxelModel object using large square faces.

This function can greatly reduce the file size of generated meshes. However, it may not correctly recognize small (1 voxel) model features and currently produces files with a nonstandard vertex arrangement. Use at your own risk.


Example:

mesh1 = vf.Mesh.simpleSquares(model1)


Args

voxel_model
VoxelModel object to be converted to a mesh
color
Mesh color in the format (r, g, b, a), None to use voxel colors

Returns

Mesh

Expand source code
@classmethod
def simpleSquares(cls, voxel_model: VoxelModel, color: Tuple[float, float, float, float] = None):
    """
    Generate a mesh object from a VoxelModel object using large square faces.

    This function can greatly reduce the file size of generated meshes. However, it may not correctly recognize
    small (1 voxel) model features and currently produces files with a nonstandard vertex arrangement. Use at your own
    risk.

    ----

    Example:

    ``mesh1 = vf.Mesh.simpleSquares(model1)``

    ----

    Args:
        voxel_model: VoxelModel object to be converted to a mesh
        color: Mesh color in the format (r, g, b, a), None to use voxel colors
    
    Returns:
        Mesh
    """
    voxel_model_fit = voxel_model.fitWorkspace()
    voxel_model_array = voxel_model_fit.voxels.astype(np.uint16)
    model_materials = voxel_model_fit.materials
    model_offsets = voxel_model_fit.coords

    x_len, y_len, z_len = voxel_model_array.shape

    # Determine vertex types
    vert_type = np.zeros((x_len + 1, y_len + 1, z_len + 1), dtype=np.uint8)
    vert_color = np.zeros((x_len + 1, y_len + 1, z_len + 1, 4), dtype=np.float32)
    for x in tqdm(range(x_len), desc='Finding voxel vertices'):
        for y in range(y_len):
            for z in range(z_len):
                if voxel_model_array[x, y, z] > 0:
                    vert_type[x:x+2, y:y+2, z:z+2] = 1 # Type 1 = occupied/exterior

                    if color is None:
                        r = 0
                        g = 0
                        b = 0

                        for i in range(voxel_model.materials.shape[1] - 1):
                            r = r + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['r']
                            g = g + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['g']
                            b = b + model_materials[voxel_model_array[x, y, z]][i + 1] * material_properties[i]['b']

                        r = 1 if r > 1 else r
                        g = 1 if g > 1 else g
                        b = 1 if b > 1 else b

                        a = 1 - model_materials[voxel_model_array[x, y, z]][1]

                        voxel_color = np.array([r, g, b, a])
                    else:
                        voxel_color = np.array(color)

                    for cx in range(x, x+2):
                        for cy in range(y, y+2):
                            for cz in range(z, z+2):
                                vert_color[cx, cy, cz, :] = voxel_color

    for x in tqdm(range(1, x_len), desc='Finding interior vertices'):
        for y in range(1, y_len):
            for z in range(1, z_len):
                vert_type = markInterior(vert_type, x, y, z)

    for x in tqdm(range(0, x_len+1), desc='Finding feature vertices'):
        for y in range(0, y_len+1):
            for z in range(0, z_len+1):
                vert_type = markInsideCorner(vert_type, x, y, z)

    # Initialize arrays
    vi = 0 # Tracks current vertex index
    verts = []
    colors = []
    tris = []
    quads = []
    vert_index = np.multiply(np.ones_like(vert_type, dtype=np.int32), -1)

    for x in tqdm(range(x_len + 1), desc='Meshing'):
        for y in range(y_len + 1):
            for z in range(z_len + 1):
                dirs = [[1, 1, 0], [1, 0, 1], [0, 1, 1]]
                for d in dirs:
                    vi, vert_type, vert_index, new_verts, new_colors, new_tris, new_quads = findSquare(vi, vert_type, vert_index, vert_color, voxel_model_array, x, y, z, d[0], d[1], d[2])
                    verts += new_verts
                    colors += new_colors
                    tris += new_tris
                    quads += new_quads

    verts = np.array(verts, dtype=np.float32)
    colors = np.array(colors, dtype=np.float32)
    tris = np.array(tris, dtype=np.uint32)
    quads = np.array(quads, dtype=np.uint32)

    # Shift model to align with origin
    verts[:, 0] = np.add(verts[:, 0], model_offsets[0])
    verts[:, 1] = np.add(verts[:, 1], model_offsets[1])
    verts[:, 2] = np.add(verts[:, 2], model_offsets[2])

    return cls(voxel_model_array, verts, colors, tris, voxel_model.resolution)

Methods

def export(self, filename: str)

Save a copy of the mesh with the specified name and file format.


Example:

mesh1.export('result.stl')


Args

filename
File name with extension

Returns

None

Expand source code
def export(self, filename: str):
    """
    Save a copy of the mesh with the specified name and file format.

    ----

    Example:

    ``mesh1.export('result.stl')``

    ----

    Args:
        filename: File name with extension
    
    Returns:
        None
    """
    # Adjust coordinate scale
    verts = np.divide(self.verts, self.res)

    cells = {
        "triangle": self.tris
    }

    output_mesh = meshio.Mesh(verts, cells)
    meshio.write(filename, output_mesh)
def plot(self, plot=None, name: str = 'mesh', wireframe: bool = True, mm_scale: bool = False, **kwargs)

Add mesh to a K3D plot in Jupyter Notebook.

Additional display options:

  • flat_shading: bool. Whether mesh should display with flat shading.
  • opacity: float. Opacity of mesh.
  • volume: array_like. 3D array of float
  • volume_bounds: array_like. 6-element tuple specifying the bounds of the volume data (x0, x1, y0, y1, z0, z1)
  • opacity_function: array. A list of float tuples (attribute value, opacity), sorted by attribute value. The first tuples should have value 0.0, the last 1.0; opacity is in the range 0.0 to 1.0.
  • side: string. Control over which side to render for a mesh. Legal values are front, back, double.
  • texture: bytes. Image data in a specific format.
  • texture_file_format: str. Format of the data, it should be the second part of MIME format of type 'image/', for example 'jpeg', 'png', 'gif', 'tiff'.
  • uvs: array_like. Array of float uvs for the texturing, coresponding to each vertex.
  • kwargs: dict. Dictionary arguments to configure transform and model_matrix.

More information available at: https://github.com/K3D-tools/K3D-jupyter

Args

plot
Plot object to add mesh to
name
Mesh name
wireframe
Enable displaying mesh as a wireframe
mm_scale
Enable to use a mm plot scale, disable to use a voxel plot scale
kwargs
Additional display options (see above)

Returns

K3D plot object

Expand source code
def plot(self, plot = None, name: str = 'mesh', wireframe: bool = True, mm_scale: bool = False, **kwargs):
    """
    Add mesh to a K3D plot in Jupyter Notebook.

    Additional display options:

    - flat_shading: `bool`. Whether mesh should display with flat shading.
    - opacity: `float`. Opacity of mesh.
    - volume: `array_like`. 3D array of `float`
    - volume_bounds: `array_like`. 6-element tuple specifying the bounds of the volume data (x0, x1, y0, y1, z0, z1)
    - opacity_function: `array`. A list of float tuples (attribute value, opacity), sorted by attribute value. The first tuples should have value 0.0, the last 1.0; opacity is in the range 0.0 to 1.0.
    - side: `string`. Control over which side to render for a mesh. Legal values are `front`, `back`, `double`.
    - texture: `bytes`. Image data in a specific format.
    - texture_file_format: `str`. Format of the data, it should be the second part of MIME format of type 'image/', for example 'jpeg', 'png', 'gif', 'tiff'.
    - uvs: `array_like`. Array of float uvs for the texturing, coresponding to each vertex.
    - kwargs: `dict`. Dictionary arguments to configure transform and model_matrix.

    More information available at: https://github.com/K3D-tools/K3D-jupyter

    Args:
        plot: Plot object to add mesh to
        name: Mesh name
        wireframe: Enable displaying mesh as a wireframe
        mm_scale: Enable to use a mm plot scale, disable to use a voxel plot scale
        kwargs: Additional display options (see above)
    
    Returns:
        K3D plot object
    """
    # Get verts
    verts = self.verts

    # Adjust coordinate scale
    if mm_scale:
        verts = np.divide(verts, self.res)

    # Get tris
    tris = self.tris

    # Get colors
    colors = []
    for c in self.colors:
        colors.append(rgb_to_hex(c[0], c[1], c[2]))
    colors = np.array(colors, dtype=np.uint32)

    # Plot
    if plot is None:
        plot = k3d.plot()

    plot += k3d.mesh(verts.astype(np.float32), tris.astype(np.uint32), colors=colors, name=name, wireframe=wireframe, **kwargs)
    return plot
def scale(self, factor: float)

Apply a scaling factor to a mesh.

Args

factor
Scaling factor

Returns

Mesh

Expand source code
def scale(self, factor: float):
    """
    Apply a scaling factor to a mesh.

    Args:
        factor: Scaling factor
    
    Returns:
        Mesh
    """
    new_mesh = Mesh.copy(self)
    new_mesh.verts = np.multiply(self.verts, factor)
    return new_mesh
def setColor(self, color: Tuple[float, float, float, float])

Change the color of a mesh.

Args

color
Mesh color in the format (r, g, b, a)

Returns

Mesh

Expand source code
def setColor(self, color: Tuple[float, float, float, float]):
    """
    Change the color of a mesh.

    Args:
        color: Mesh color in the format (r, g, b, a)
    
    Returns:
        Mesh
    """
    new_mesh = Mesh.copy(self)
    new_mesh.colors = generateColors(len(self.verts), color)
    return new_mesh
def setResolution(self, resolution: float)

Change the defined resolution of a mesh.

The mesh resolution will determine the scale of plots and exported mesh files.

Args

resolution
Number of voxels per mm (higher number = finer resolution)

Returns

Mesh

Expand source code
def setResolution(self, resolution: float):
    """
    Change the defined resolution of a mesh.

    The mesh resolution will determine the scale of plots and exported mesh files.

    Args:
        resolution: Number of voxels per mm (higher number = finer resolution)
    
    Returns:
        Mesh
    """
    new_mesh = Mesh.copy(self)
    new_mesh.res = resolution
    return new_mesh
def simplify(self, percent_verts: float, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1))

Simplify a mesh to contain a given percentage of the original number of vertices.

More information on the simplification algorithm is available at: https://github.com/jannessm/quadric-mesh-simplification

Args

percent_verts
Percentage of vertex count allowed in the result mesh, 0-1
color
Mesh color in the format (r, g, b, a)

Returns

Mesh

Expand source code
def simplify(self, percent_verts: float, color: Tuple[float, float, float, float] = (0.8, 0.8, 0.8, 1)):
    """
    Simplify a mesh to contain a given percentage of the original number of vertices.

    More information on the simplification algorithm is available at: https://github.com/jannessm/quadric-mesh-simplification

    Args:
        percent_verts: Percentage of vertex count allowed in the result mesh, 0-1
        color: Mesh color in the format (r, g, b, a)
    
    Returns:
        Mesh
    """
    num_verts = self.verts.shape[0]
    target_verts = num_verts * percent_verts

    new_verts, new_tris = simplify_mesh(positions=self.verts.astype(np.double), face=self.tris.astype(np.uint32), num_nodes=target_verts)
    verts_colors = generateColors(len(new_verts), color)

    return Mesh(np.copy(self.model), new_verts, verts_colors, new_tris, self.res)
def translate(self, vector: Tuple[float, float, float])

Translate a model by the specified vector.

Args

vector
Translation vector in voxels

Returns

Mesh

Expand source code
def translate(self, vector: Tuple[float, float, float]):
    """
    Translate a model by the specified vector.

    Args:
        vector: Translation vector in voxels
    
    Returns:
        Mesh
    """
    new_mesh = Mesh.copy(self)
    new_mesh.verts[:, 0] = np.add(self.verts[:, 0], vector[0])
    new_mesh.verts[:, 1] = np.add(self.verts[:, 1], vector[1])
    new_mesh.verts[:, 2] = np.add(self.verts[:, 2], vector[2])
    return new_mesh
def translateMM(self, vector: Tuple[float, float, float])

Translate a model by the specified vector.

Args

vector
Translation vector in mm

Returns

Mesh

Expand source code
def translateMM(self, vector: Tuple[float, float, float]):
    """
    Translate a model by the specified vector.

    Args:
        vector: Translation vector in mm
    
    Returns:
        Mesh
    """
    xV = vector[0] * self.res
    yV = vector[1] * self.res
    zV = vector[2] * self.res
    new_mesh = self.translate((xV, yV, zV))
    return new_mesh
def viewer(self, grids: bool = False, drawEdges: bool = True, edgeColor: Tuple[float, float, float, float] = (0, 0, 0, 0.5), positionOffset: Tuple[int, int, int] = (0, 0, 0), viewAngle: Tuple[int, int, int] = (40, 30, 300), resolution: Tuple[int, int] = (1280, 720), name: str = 'Plot 1', export: bool = False)

Display the mesh in a 3D viewer window.

This function will block program execution until viewer window is closed

Args

grids
Enable/disable display of XYZ axes and grids
drawEdges
Enable/disable display of voxel edges
edgeColor
Set display color of voxel edges
positionOffset
Offset of the camera target from the center of the model in voxels
viewAngle
Elevation, Azimuth, and Distance of the camera
resolution
Window resolution in px
name
Plot window name
export
Enable/disable exporting a screenshot of the plot

Returns

None

Expand source code
def viewer(self, grids: bool = False, drawEdges: bool = True,
           edgeColor: Tuple[float, float, float, float] = (0, 0, 0, 0.5),
           positionOffset: Tuple[int, int, int] = (0, 0, 0), viewAngle: Tuple[int, int, int] = (40, 30, 300),
           resolution: Tuple[int, int] = (1280, 720), name: str = 'Plot 1', export: bool = False):
    """
    Display the mesh in a 3D viewer window.

    This function will block program execution until viewer window is closed

    Args:
        grids: Enable/disable display of XYZ axes and grids
        drawEdges: Enable/disable display of voxel edges
        edgeColor: Set display color of voxel edges
        positionOffset: Offset of the camera target from the center of the model in voxels
        viewAngle: Elevation, Azimuth, and Distance of the camera
        resolution: Window resolution in px
        name: Plot window name
        export: Enable/disable exporting a screenshot of the plot
    
    Returns:
        None
    """
    app = qtw.QApplication(sys.argv)

    mesh_data = pgo.MeshData(vertexes=self.verts, faces=self.tris, vertexColors=self.colors, faceColors=None)
    mesh_item = pgo.GLMeshItem(meshdata=mesh_data, shader='balloon', drawEdges=drawEdges, edgeColor=edgeColor,
                               smooth=False, computeNormals=False, glOptions='translucent')

    widget = pgo.GLViewWidget()
    widget.setBackgroundColor('w')
    widget.addItem(mesh_item)

    if grids:
        # Add grids
        gx = pgo.GLGridItem()
        gx.setSize(x=50, y=50, z=50)
        gx.rotate(90, 0, 1, 0)
        gx.translate(-0.5, 24.5, 24.5)
        widget.addItem(gx)
        gy = pgo.GLGridItem()
        gy.setSize(x=50, y=50, z=50)
        gy.rotate(90, 1, 0, 0)
        gy.translate(24.5, -0.5, 24.5)
        widget.addItem(gy)
        gz = pgo.GLGridItem()
        gz.setSize(x=50, y=50, z=50)
        gz.translate(24.5, 24.5, -0.5)
        widget.addItem(gz)

        # Add axes
        ptsx = np.array([[-0.5, -0.5, -0.5], [50, -0.5, -0.5]])
        pltx = pgo.GLLinePlotItem(pos=ptsx, color=(1, 0, 0, 1), width=1, antialias=True)
        widget.addItem(pltx)
        ptsy = np.array([[-0.5, -0.5, -0.5], [-0.5, 50, -0.5]])
        plty = pgo.GLLinePlotItem(pos=ptsy, color=(0, 1, 0, 1), width=1, antialias=True)
        widget.addItem(plty)
        ptsz = np.array([[-0.5, -0.5, -0.5], [-0.5, -0.5, 50]])
        pltz = pgo.GLLinePlotItem(pos=ptsz, color=(0, 0, 1, 1), width=1, antialias=True)
        widget.addItem(pltz)

    # Set plot options
    widget.opts['center'] = qg.QVector3D(((self.model.shape[0] / self.res) / 2) + positionOffset[0],
                                         ((self.model.shape[1] / self.res) / 2) + positionOffset[1],
                                         ((self.model.shape[2] / self.res) / 2) + positionOffset[2])
    widget.opts['elevation'] = viewAngle[0]
    widget.opts['azimuth'] = viewAngle[1]
    widget.opts['distance'] = viewAngle[2]
    widget.resize(resolution[0], resolution[1])

    # Show plot
    widget.setWindowTitle(str(name))
    widget.show()

    app.processEvents()

    # if export: # TODO: Fix export code
    #     widget.paintGL()
    #     widget.grabFrameBuffer().save(str(name) + '.png')

    print('Close viewer to resume program')
    app.exec_()
    app.quit()