Skip to content

Utilities

Helper functions for I/O, geometry operations, and more.

utilities

Helper utilities for I/O, geometry operations, and more.

TerminalCommand

TerminalCommand(cmd, cwd=None, env=None)

Creates a new command container. Note that this immediately executes the command synchronously and writes the return values to the corresponding members.

Attributes:

Name Type Description
cmd The command to execute.
cwd The working directory to run the command in.
env The environment the command is run in.
Source code in src/compas_slicer/utilities/terminal_command.py
def __init__(self, cmd, cwd=None, env=None):
    """
    Creates a new command container.
    Note that this immediately executes the command synchronously and writes the
    return values to the corresponding members.

    Attributes
    ----------
    cmd : The command to execute.
    cwd : The working directory to run the command in.
    env : The environment the command is run in.
    """
    process = p.Popen(cmd, stdout=p.PIPE, stderr=p.PIPE, shell=True, cwd=cwd, env=env)
    stdout, stderr = process.communicate()

    self.stdout = stdout.decode('utf8')
    self.stderr = stderr.decode('utf8')

    self.return_code = process.returncode
    process.kill()

transfer_mesh_attributes_to_printpoints

transfer_mesh_attributes_to_printpoints(mesh, printpoints)

Transfers face and vertex attributes from the mesh to the printpoints. Each printpoint is projected to the closest mesh face. It takes directly all the face attributes. It takes the averaged vertex attributes of the face vertices using barycentric coordinates.

Face attributes can be anything. Vertex attributes can only be entities that support multiplication with a scalar. They have been tested to work with scalars and np.arrays.

The reserved attribute names (see 'is_reserved_attribute(attr)') are not passed on to the printpoints.

Parameters:

Name Type Description Default
mesh Mesh

The mesh to transfer attributes from.

required
printpoints PrintPointsCollection

The collection of printpoints to transfer attributes to.

required
Source code in src/compas_slicer/utilities/attributes_transfer.py
def transfer_mesh_attributes_to_printpoints(
    mesh: Mesh,
    printpoints: PrintPointsCollection,
) -> None:
    """
    Transfers face and vertex attributes from the mesh to the printpoints.
    Each printpoint is projected to the closest mesh face. It takes directly all the face attributes.
    It takes the averaged vertex attributes of the face vertices using barycentric coordinates.

    Face attributes can be anything.
    Vertex attributes can only be entities that support multiplication with a scalar. They have been tested to work
    with scalars and np.arrays.

    The reserved attribute names (see 'is_reserved_attribute(attr)') are not passed on to the printpoints.

    Parameters
    ----------
    mesh : Mesh
        The mesh to transfer attributes from.
    printpoints : PrintPointsCollection
        The collection of printpoints to transfer attributes to.

    """
    logger.info('Transferring mesh attributes to the printpoints.')

    all_pts = [ppt.pt for ppt in printpoints.iter_printpoints()]

    closest_fks, projected_pts = pull_pts_to_mesh_faces(mesh, all_pts)

    i = 0
    with progressbar.ProgressBar(max_value=len(all_pts)) as bar:
        for pp in printpoints.iter_printpoints():
            fkey = closest_fks[i]
            proj_pt = projected_pts[i]
            pp.attributes = transfer_mesh_attributes_to_point(mesh, fkey, proj_pt)
            i += 1
            bar.update(i)

remap

remap(input_val, in_from, in_to, out_from, out_to)

Bounded remap from source domain to target domain.

Source code in src/compas_slicer/utilities/utils.py
def remap(input_val: float, in_from: float, in_to: float, out_from: float, out_to: float) -> float:
    """Bounded remap from source domain to target domain."""
    if input_val <= in_from:
        return out_from
    elif input_val >= in_to:
        return out_to
    else:
        return remap_unbound(input_val, in_from, in_to, out_from, out_to)

remap_unbound

remap_unbound(input_val, in_from, in_to, out_from, out_to)

Remap input_val from source domain to target domain (no clamping).

Source code in src/compas_slicer/utilities/utils.py
def remap_unbound(input_val: float, in_from: float, in_to: float, out_from: float, out_to: float) -> float:
    """Remap input_val from source domain to target domain (no clamping)."""
    out_range = out_to - out_from
    in_range = in_to - in_from
    in_val = input_val - in_from
    val = (float(in_val) / in_range) * out_range
    out_val = out_from + val
    return out_val

get_output_directory

get_output_directory(path)

Get or create 'output' directory in the given path.

Parameters:

Name Type Description Default
path str | Path

The path where the 'output' directory will be created.

required

Returns:

Type Description
Path

The path to the 'output' directory.

Source code in src/compas_slicer/utilities/utils.py
def get_output_directory(path: str | Path) -> Path:
    """Get or create 'output' directory in the given path.

    Parameters
    ----------
    path : str | Path
        The path where the 'output' directory will be created.

    Returns
    -------
    Path
        The path to the 'output' directory.

    """
    output_dir = Path(path) / 'output'
    output_dir.mkdir(exist_ok=True)
    return output_dir

get_closest_pt_index

get_closest_pt_index(pt, pts)

Find the index of the closest point to pt in pts.

Parameters:

Name Type Description Default
pt Point | NDArray

Query point.

required
pts list[Point] | NDArray

Point cloud to search.

required

Returns:

Type Description
int

Index of the closest point.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_pt_index(pt: Point | NDArray, pts: list[Point] | NDArray) -> int:
    """Find the index of the closest point to pt in pts.

    Parameters
    ----------
    pt : Point | NDArray
        Query point.
    pts : list[Point] | NDArray
        Point cloud to search.

    Returns
    -------
    int
        Index of the closest point.

    """
    ci: int = closest_point_in_cloud(point=pt, cloud=pts)[2]
    return ci

get_closest_pt

get_closest_pt(pt, pts)

Find the closest point to pt in pts.

Parameters:

Name Type Description Default
pt Point | NDArray

Query point.

required
pts list[Point]

Point cloud to search.

required

Returns:

Type Description
Point

The closest point.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_pt(pt: Point | NDArray, pts: list[Point]) -> Point:
    """Find the closest point to pt in pts.

    Parameters
    ----------
    pt : Point | NDArray
        Query point.
    pts : list[Point]
        Point cloud to search.

    Returns
    -------
    Point
        The closest point.

    """
    ci = closest_point_in_cloud(point=pt, cloud=pts)[2]
    return pts[ci]

pull_pts_to_mesh_faces

pull_pts_to_mesh_faces(mesh, points)

Project points to mesh and find their closest face keys.

Parameters:

Name Type Description Default
mesh Mesh

The mesh to project onto.

required
points list[Point]

Points to project.

required

Returns:

Type Description
tuple[list[int], list[Point]]

Closest face keys and projected points.

Source code in src/compas_slicer/utilities/utils.py
def pull_pts_to_mesh_faces(mesh: Mesh, points: list[Point]) -> tuple[list[int], list[Point]]:
    """Project points to mesh and find their closest face keys.

    Parameters
    ----------
    mesh : Mesh
        The mesh to project onto.
    points : list[Point]
        Points to project.

    Returns
    -------
    tuple[list[int], list[Point]]
        Closest face keys and projected points.

    """
    points_arr = np.array(points, dtype=np.float64).reshape((-1, 3))
    fi_fk = dict(enumerate(mesh.faces()))
    f_centroids = np.array([mesh.face_centroid(fkey) for fkey in mesh.faces()], dtype=np.float64)
    closest_fis = np.argmin(scipy.spatial.distance_matrix(points_arr, f_centroids), axis=1)
    closest_fks = [fi_fk[fi] for fi in closest_fis]
    projected_pts = [closest_point_on_plane(point, mesh.face_plane(fi)) for point, fi in zip(points_arr, closest_fis)]
    return closest_fks, projected_pts

smooth_vectors

smooth_vectors(vectors, strength, iterations)

Smooth vectors iteratively.

Parameters:

Name Type Description Default
vectors list[Vector]

Vectors to smooth.

required
strength float

Smoothing strength (0-1).

required
iterations int

Number of smoothing iterations.

required

Returns:

Type Description
list[Vector]

Smoothed vectors.

Source code in src/compas_slicer/utilities/utils.py
def smooth_vectors(vectors: list[Vector], strength: float, iterations: int) -> list[Vector]:
    """Smooth vectors iteratively.

    Parameters
    ----------
    vectors : list[Vector]
        Vectors to smooth.
    strength : float
        Smoothing strength (0-1).
    iterations : int
        Number of smoothing iterations.

    Returns
    -------
    list[Vector]
        Smoothed vectors.

    """
    for _ in range(iterations):
        for i, n in enumerate(vectors):
            if 0 < i < len(vectors) - 1:
                neighbors_average = (vectors[i - 1] + vectors[i + 1]) * 0.5
            else:
                neighbors_average = n
            vectors[i] = n * (1 - strength) + neighbors_average * strength
    return vectors

save_to_json

save_to_json(data, filepath, name)

Save data to JSON file.

Parameters:

Name Type Description Default
data dict | list

Data to save.

required
filepath str | Path

Directory path.

required
name str

Filename.

required
Source code in src/compas_slicer/utilities/utils.py
def save_to_json(
    data: dict[str, Any] | dict[int, Any] | list[Any], filepath: str | Path, name: str
) -> None:
    """Save data to JSON file.

    Parameters
    ----------
    data : dict | list
        Data to save.
    filepath : str | Path
        Directory path.
    name : str
        Filename.

    """
    filename = Path(filepath) / name
    logger.info(f"Saving to json: {filename}")
    filename.write_text(json.dumps(data, indent=3, sort_keys=True))

load_from_json

load_from_json(filepath, name)

Load data from JSON file.

Parameters:

Name Type Description Default
filepath str | Path

Directory path.

required
name str

Filename.

required

Returns:

Type Description
Any

Loaded data.

Source code in src/compas_slicer/utilities/utils.py
def load_from_json(filepath: str | Path, name: str) -> Any:
    """Load data from JSON file.

    Parameters
    ----------
    filepath : str | Path
        Directory path.
    name : str
        Filename.

    Returns
    -------
    Any
        Loaded data.

    """
    filename = Path(filepath) / name
    data = json.loads(filename.read_text())
    logger.info(f"Loaded json: {filename}")
    return data

is_jsonable

is_jsonable(x)

Return True if x can be JSON-serialized.

Source code in src/compas_slicer/utilities/utils.py
def is_jsonable(x: Any) -> bool:
    """Return True if x can be JSON-serialized."""
    try:
        json.dumps(x)
        return True
    except TypeError:
        return False

get_jsonable_attributes

get_jsonable_attributes(attributes_dict)

Convert attributes dict to JSON-serializable form.

Source code in src/compas_slicer/utilities/utils.py
def get_jsonable_attributes(attributes_dict: dict[str, Any]) -> dict[str, Any]:
    """Convert attributes dict to JSON-serializable form."""
    jsonable_attr: dict[str, Any] = {}
    for attr_key in attributes_dict:
        attr = attributes_dict[attr_key]
        if is_jsonable(attr):
            jsonable_attr[attr_key] = attr
        else:
            if isinstance(attr, np.ndarray):
                jsonable_attr[attr_key] = list(attr)
            else:
                jsonable_attr[attr_key] = 'non serializable attribute'
    return jsonable_attr

save_to_text_file

save_to_text_file(data, filepath, name)

Save text to file.

Parameters:

Name Type Description Default
data str

Text to save.

required
filepath str | Path

Directory path.

required
name str

Filename.

required
Source code in src/compas_slicer/utilities/utils.py
def save_to_text_file(data: str, filepath: str | Path, name: str) -> None:
    """Save text to file.

    Parameters
    ----------
    data : str
        Text to save.
    filepath : str | Path
        Directory path.
    name : str
        Filename.

    """
    filename = Path(filepath) / name
    logger.info(f"Saving to text file: {filename}")
    filename.write_text(data)

get_closest_mesh_vkey_to_pt

get_closest_mesh_vkey_to_pt(mesh, pt)

Find the vertex key closest to the point.

Parameters:

Name Type Description Default
mesh Mesh

The mesh.

required
pt Point

Query point.

required

Returns:

Type Description
int

Closest vertex key.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_mesh_vkey_to_pt(mesh: Mesh, pt: Point) -> int:
    """Find the vertex key closest to the point.

    Parameters
    ----------
    mesh : Mesh
        The mesh.
    pt : Point
        Query point.

    Returns
    -------
    int
        Closest vertex key.

    """
    vertex_tupples = [(v_key, Point(data['x'], data['y'], data['z'])) for v_key, data in mesh.vertices(data=True)]
    vertex_tupples = sorted(vertex_tupples, key=lambda v_tupple: distance_point_point_sqrd(pt, v_tupple[1]))
    closest_vkey: int = vertex_tupples[0][0]
    return closest_vkey

get_closest_mesh_normal_to_pt

get_closest_mesh_normal_to_pt(mesh, pt)

Find the closest vertex normal to the point.

Parameters:

Name Type Description Default
mesh Mesh

The mesh.

required
pt Point

Query point.

required

Returns:

Type Description
Vector

Normal at closest vertex.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_mesh_normal_to_pt(mesh: Mesh, pt: Point) -> Vector:
    """Find the closest vertex normal to the point.

    Parameters
    ----------
    mesh : Mesh
        The mesh.
    pt : Point
        Query point.

    Returns
    -------
    Vector
        Normal at closest vertex.

    """
    closest_vkey = get_closest_mesh_vkey_to_pt(mesh, pt)
    v = mesh.vertex_normal(closest_vkey)
    return Vector(v[0], v[1], v[2])

get_mesh_vertex_coords_with_attribute

get_mesh_vertex_coords_with_attribute(mesh, attr, value)

Get coordinates of vertices where attribute equals value.

Parameters:

Name Type Description Default
mesh Mesh

The mesh.

required
attr str

Attribute name.

required
value Any

Value to match.

required

Returns:

Type Description
list[Point]

Points of matching vertices.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_vertex_coords_with_attribute(mesh: Mesh, attr: str, value: Any) -> list[Point]:
    """Get coordinates of vertices where attribute equals value.

    Parameters
    ----------
    mesh : Mesh
        The mesh.
    attr : str
        Attribute name.
    value : Any
        Value to match.

    Returns
    -------
    list[Point]
        Points of matching vertices.

    """
    pts: list[Point] = []
    for vkey, data in mesh.vertices(data=True):
        if data[attr] == value:
            pts.append(Point(*mesh.vertex_coordinates(vkey)))
    return pts

get_normal_of_path_on_xy_plane

get_normal_of_path_on_xy_plane(k, point, path, mesh)

Find the normal of the curve on xy plane at point with index k.

Parameters:

Name Type Description Default
k int

Index of the point.

required
point Point

The point.

required
path Path

The path containing the point.

required
mesh Mesh

The mesh (fallback for degenerate cases).

required

Returns:

Type Description
Vector

Normal vector.

Source code in src/compas_slicer/utilities/utils.py
def get_normal_of_path_on_xy_plane(k: int, point: Point, path: SlicerPath, mesh: Mesh) -> Vector:
    """Find the normal of the curve on xy plane at point with index k.

    Parameters
    ----------
    k : int
        Index of the point.
    point : Point
        The point.
    path : SlicerPath
        The path containing the point.
    mesh : Mesh
        The mesh (fallback for degenerate cases).

    Returns
    -------
    Vector
        Normal vector.

    """
    # find mesh normal is not really needed in the 2D case of planar slicer
    # instead we only need the normal of the curve based on the neighboring pts
    if (0 < k < len(path.points) - 1) or path.is_closed:
        prev_pt = path.points[k - 1]
        next_pt = path.points[(k + 1) % len(path.points)]
        v1 = np.array(normalize_vector(Vector.from_start_end(prev_pt, point)))
        v2 = np.array(normalize_vector(Vector.from_start_end(point, next_pt)))
        v = (v1 + v2) * 0.5
        normal = [-v[1], v[0], v[2]]  # rotate 90 degrees COUNTER-clockwise on the xy plane

    else:
        if k == 0:
            next_pt = path.points[k + 1]
            v = normalize_vector(Vector.from_start_end(point, next_pt))
            normal = [-v[1], v[0], v[2]]  # rotate 90 degrees COUNTER-clockwise on the xy plane
        else:  # k == len(path.points)-1:
            prev_pt = path.points[k - 1]
            v = normalize_vector(Vector.from_start_end(point, prev_pt))
            normal = [v[1], -v[0], v[2]]  # rotate 90 degrees clockwise on the xy plane

    if length_vector(normal) == 0:
        # When the neighboring elements happen to cancel out, then search for the true normal,
        # and project it on the xy plane for consistency
        normal = get_closest_mesh_normal_to_pt(mesh, point)
        normal = [normal[0], normal[1], 0]

    normal = normalize_vector(normal)
    normal = Vector(*list(normal))
    return normal

get_mesh_cotmatrix

get_mesh_cotmatrix(mesh, fix_boundaries=True)

Get the cotangent Laplacian matrix of the mesh.

Computes L_ij = (cot α_ij + cot β_ij) / 2 for adjacent vertices, with L_ii = -sum_j L_ij (row sum = 0).

Parameters:

Name Type Description Default
mesh Mesh

The mesh (must be triangulated).

required
fix_boundaries bool

If True, zero out rows for boundary vertices.

True

Returns:

Type Description
csr_matrix

Sparse matrix (V x V), cotangent Laplacian.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_cotmatrix(mesh: Mesh, fix_boundaries: bool = True) -> csr_matrix:
    """Get the cotangent Laplacian matrix of the mesh.

    Computes L_ij = (cot α_ij + cot β_ij) / 2 for adjacent vertices,
    with L_ii = -sum_j L_ij (row sum = 0).

    Parameters
    ----------
    mesh : Mesh
        The mesh (must be triangulated).
    fix_boundaries : bool
        If True, zero out rows for boundary vertices.

    Returns
    -------
    csr_matrix
        Sparse matrix (V x V), cotangent Laplacian.

    """
    V, F = mesh.to_vertices_and_faces()
    vertices = np.array(V, dtype=np.float64)
    faces = np.array(F, dtype=np.int32)

    n_vertices = len(vertices)

    # Get cotangent weights for each half-edge
    # For each face, compute cotangents of all three angles
    i0, i1, i2 = faces[:, 0], faces[:, 1], faces[:, 2]
    v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]

    # Edge vectors
    e0 = v2 - v1  # opposite to vertex 0
    e1 = v0 - v2  # opposite to vertex 1
    e2 = v1 - v0  # opposite to vertex 2

    # Cotangent of angle at vertex i = dot(e_j, e_k) / |cross(e_j, e_k)|
    # where e_j and e_k are edges adjacent to vertex i
    def cotangent(a: NDArray, b: NDArray) -> NDArray:
        cross = np.cross(a, b)
        cross_norm = np.linalg.norm(cross, axis=1)
        dot = np.sum(a * b, axis=1)
        # Avoid division by zero
        cross_norm = np.maximum(cross_norm, 1e-10)
        return dot / cross_norm

    # Cotangent at each vertex of each face
    cot0 = cotangent(-e2, e1)  # angle at vertex 0
    cot1 = cotangent(-e0, e2)  # angle at vertex 1
    cot2 = cotangent(-e1, e0)  # angle at vertex 2

    # Build sparse matrix
    # L_ij += 0.5 * cot(angle opposite to edge ij)
    row = np.concatenate([i0, i1, i1, i2, i2, i0])
    col = np.concatenate([i1, i0, i2, i1, i0, i2])
    data = np.concatenate([cot2, cot2, cot0, cot0, cot1, cot1]) * 0.5

    L = csr_matrix((data, (row, col)), shape=(n_vertices, n_vertices))

    # Make symmetric and set diagonal to negative row sum
    L = L + L.T
    L = L - scipy.sparse.diags(np.array(L.sum(axis=1)).flatten())

    if fix_boundaries:
        # Zero out rows for boundary vertices
        boundary_mask = np.zeros(n_vertices, dtype=bool)
        for i, (_vkey, vdata) in enumerate(mesh.vertices(data=True)):
            if vdata.get('boundary', 0) > 0:
                boundary_mask[i] = True

        if np.any(boundary_mask):
            L = L.tolil()
            for i in np.where(boundary_mask)[0]:
                L[i, :] = 0
            L = L.tocsr()

    return L

get_mesh_cotans

get_mesh_cotans(mesh)

Get the cotangent entries of the mesh.

Parameters:

Name Type Description Default
mesh Mesh

The mesh (must be triangulated).

required

Returns:

Type Description
NDArray

F x 3 array of ½*cotangents for corresponding angles. Column i contains cotangent of angle at vertex i of each face.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_cotans(mesh: Mesh) -> NDArray:
    """Get the cotangent entries of the mesh.

    Parameters
    ----------
    mesh : Mesh
        The mesh (must be triangulated).

    Returns
    -------
    NDArray
        F x 3 array of 1/2*cotangents for corresponding angles.
        Column i contains cotangent of angle at vertex i of each face.

    """
    V, F = mesh.to_vertices_and_faces()
    vertices = np.array(V, dtype=np.float64)
    faces = np.array(F, dtype=np.int32)

    i0, i1, i2 = faces[:, 0], faces[:, 1], faces[:, 2]
    v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]

    e0 = v2 - v1
    e1 = v0 - v2
    e2 = v1 - v0

    def cotangent(a: NDArray, b: NDArray) -> NDArray:
        cross = np.cross(a, b)
        cross_norm = np.linalg.norm(cross, axis=1)
        dot = np.sum(a * b, axis=1)
        cross_norm = np.maximum(cross_norm, 1e-10)
        return dot / cross_norm

    cot0 = cotangent(-e2, e1)
    cot1 = cotangent(-e0, e2)
    cot2 = cotangent(-e1, e0)

    return np.column_stack([cot0, cot1, cot2]) * 0.5

get_mesh_massmatrix

get_mesh_massmatrix(mesh)

Get the mass matrix of the mesh (Voronoi area weights).

Parameters:

Name Type Description Default
mesh Mesh

The mesh (must be triangulated).

required

Returns:

Type Description
csr_matrix

Sparse diagonal matrix (V x V), vertex areas.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_massmatrix(mesh: Mesh) -> csr_matrix:
    """Get the mass matrix of the mesh (Voronoi area weights).

    Parameters
    ----------
    mesh : Mesh
        The mesh (must be triangulated).

    Returns
    -------
    csr_matrix
        Sparse diagonal matrix (V x V), vertex areas.

    """
    V, F = mesh.to_vertices_and_faces()
    vertices = np.array(V, dtype=np.float64)
    faces = np.array(F, dtype=np.int32)

    n_vertices = len(vertices)

    # Compute face areas
    i0, i1, i2 = faces[:, 0], faces[:, 1], faces[:, 2]
    v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]

    cross = np.cross(v1 - v0, v2 - v0)
    face_areas = 0.5 * np.linalg.norm(cross, axis=1)

    # Distribute 1/3 of each face area to each vertex
    vertex_areas = np.zeros(n_vertices)
    np.add.at(vertex_areas, i0, face_areas / 3)
    np.add.at(vertex_areas, i1, face_areas / 3)
    np.add.at(vertex_areas, i2, face_areas / 3)

    return scipy.sparse.diags(vertex_areas)

point_list_to_dict

point_list_to_dict(pts_list)

Convert list of points/vectors to dict for JSON.

Parameters:

Name Type Description Default
pts_list list[Point | Vector]

List of points or vectors.

required

Returns:

Type Description
dict[int, list[float]]

Dict mapping index to [x, y, z].

Source code in src/compas_slicer/utilities/utils.py
def point_list_to_dict(pts_list: list[Point | Vector]) -> dict[int, list[float]]:
    """Convert list of points/vectors to dict for JSON.

    Parameters
    ----------
    pts_list : list[Point | Vector]
        List of points or vectors.

    Returns
    -------
    dict[int, list[float]]
        Dict mapping index to [x, y, z].

    """
    data: dict[int, list[float]] = {}
    for i in range(len(pts_list)):
        data[i] = list(pts_list[i])
    return data

point_list_from_dict

point_list_from_dict(data)

Convert dict of points to list of [x, y, z].

Parameters:

Name Type Description Default
data dict[Any, list[float]]

Dict mapping keys to [x, y, z].

required

Returns:

Type Description
list[list[float]]

List of [x, y, z] coordinates.

Source code in src/compas_slicer/utilities/utils.py
def point_list_from_dict(data: dict[Any, list[float]]) -> list[list[float]]:
    """Convert dict of points to list of [x, y, z].

    Parameters
    ----------
    data : dict[Any, list[float]]
        Dict mapping keys to [x, y, z].

    Returns
    -------
    list[list[float]]
        List of [x, y, z] coordinates.

    """
    return [[data[i][0], data[i][1], data[i][2]] for i in data]

flattened_list_of_dictionary

flattened_list_of_dictionary(dictionary)

Flatten dictionary values into a single list.

Parameters:

Name Type Description Default
dictionary dict[Any, list[Any]]

Dictionary with list values.

required

Returns:

Type Description
list[Any]

Flattened list.

Source code in src/compas_slicer/utilities/utils.py
def flattened_list_of_dictionary(dictionary: dict[Any, list[Any]]) -> list[Any]:
    """Flatten dictionary values into a single list.

    Parameters
    ----------
    dictionary : dict[Any, list[Any]]
        Dictionary with list values.

    Returns
    -------
    list[Any]
        Flattened list.

    """
    flattened_list: list[Any] = []
    for key in dictionary:
        for item in dictionary[key]:
            flattened_list.append(item)
    return flattened_list

get_dict_key_from_value

get_dict_key_from_value(dictionary, val)

Return the key of a dictionary that stores the value.

Parameters:

Name Type Description Default
dictionary dict

The dictionary to search.

required
val Any

Value to find.

required

Returns:

Type Description
Any | None

The key, or None if not found.

Source code in src/compas_slicer/utilities/utils.py
def get_dict_key_from_value(dictionary: dict[Any, Any], val: Any) -> Any | None:
    """Return the key of a dictionary that stores the value.

    Parameters
    ----------
    dictionary : dict
        The dictionary to search.
    val : Any
        Value to find.

    Returns
    -------
    Any | None
        The key, or None if not found.

    """
    for key in dictionary:
        value = dictionary[key]
        if val == value:
            return key
    return None

find_next_printpoint

find_next_printpoint(printpoints, i, j, k)

Returns the next printpoint from the current printpoint if it exists, otherwise returns None.

Parameters:

Name Type Description Default
printpoints PrintPointsCollection

The collection of printpoints.

required
i int

Layer index.

required
j int

Path index.

required
k int

Printpoint index within the path.

required

Returns:

Type Description
PrintPoint | None

The next printpoint or None if at the end.

Source code in src/compas_slicer/utilities/utils.py
def find_next_printpoint(
    printpoints: PrintPointsCollection, i: int, j: int, k: int
) -> PrintPoint | None:
    """
    Returns the next printpoint from the current printpoint if it exists, otherwise returns None.

    Parameters
    ----------
    printpoints : PrintPointsCollection
        The collection of printpoints.
    i : int
        Layer index.
    j : int
        Path index.
    k : int
        Printpoint index within the path.

    Returns
    -------
    PrintPoint | None
        The next printpoint or None if at the end.

    """
    next_ppt = None
    if k < len(printpoints[i][j]) - 1:  # If there are more ppts in the current path
        next_ppt = printpoints[i][j][k + 1]
    else:
        if j < len(printpoints[i]) - 1:  # Otherwise take the next path
            next_ppt = printpoints[i][j + 1][0]
        else:
            if i < len(printpoints) - 1:  # Otherwise take the next layer
                next_ppt = printpoints[i + 1][0][0]
    return next_ppt

find_previous_printpoint

find_previous_printpoint(printpoints, i, j, k)

Returns the previous printpoint from the current printpoint if it exists, otherwise returns None.

Parameters:

Name Type Description Default
printpoints PrintPointsCollection

The collection of printpoints.

required
i int

Layer index.

required
j int

Path index.

required
k int

Printpoint index within the path.

required

Returns:

Type Description
PrintPoint | None

The previous printpoint or None if at the start.

Source code in src/compas_slicer/utilities/utils.py
def find_previous_printpoint(
    printpoints: PrintPointsCollection, i: int, j: int, k: int
) -> PrintPoint | None:
    """
    Returns the previous printpoint from the current printpoint if it exists, otherwise returns None.

    Parameters
    ----------
    printpoints : PrintPointsCollection
        The collection of printpoints.
    i : int
        Layer index.
    j : int
        Path index.
    k : int
        Printpoint index within the path.

    Returns
    -------
    PrintPoint | None
        The previous printpoint or None if at the start.

    """
    prev_ppt = None
    if k > 0:  # If not the first point in a path
        prev_ppt = printpoints[i][j][k - 1]
    else:
        if j > 0:  # Otherwise take the last point of the previous path
            prev_ppt = printpoints[i][j - 1][-1]
        else:
            if i > 0:  # Otherwise take the last path of the previous layer
                prev_ppt = printpoints[i - 1][-1][-1]
    return prev_ppt

interrupt

interrupt()

Interrupts the flow of the code while it is running. It asks for the user to press a enter to continue or abort.

Source code in src/compas_slicer/utilities/utils.py
def interrupt() -> None:
    """
    Interrupts the flow of the code while it is running.
    It asks for the user to press a enter to continue or abort.
    """
    value = input("Press enter to continue, Press 1 to abort ")
    if isinstance(value, str) and value == '1':
        raise ValueError("Aborted")

get_all_files_with_name

get_all_files_with_name(startswith, endswith, DATA_PATH)

Finds all the filenames in the DATA_PATH that start and end with the provided strings

Parameters:

Name Type Description Default
startswith str
required
endswith str
required
DATA_PATH str | Path
required

Returns:

Type Description
list[str]

All the filenames

Source code in src/compas_slicer/utilities/utils.py
def get_all_files_with_name(
    startswith: str, endswith: str, DATA_PATH: str | Path
) -> list[str]:
    """
    Finds all the filenames in the DATA_PATH that start and end with the provided strings

    Parameters
    ----------
    startswith: str
    endswith: str
    DATA_PATH: str | Path

    Returns
    ----------
    list[str]
        All the filenames
    """
    files = [f.name for f in Path(DATA_PATH).iterdir()
             if f.name.startswith(startswith) and f.name.endswith(endswith)]
    logger.info(f'Reloading: {files}')
    return files

check_package_is_installed

check_package_is_installed(package_name)

Throws an error if igl python bindings are not installed in the current environment.

Source code in src/compas_slicer/utilities/utils.py
def check_package_is_installed(package_name: str) -> None:
    """ Throws an error if igl python bindings are not installed in the current environment. """
    packages = TerminalCommand('conda list').get_split_output_strings()
    if package_name not in packages:
        raise PluginNotInstalledError(" ATTENTION! Package : " + package_name +
                                      " is missing! Please follow installation guide to install it.")

attributes_transfer

transfer_mesh_attributes_to_printpoints

transfer_mesh_attributes_to_printpoints(mesh, printpoints)

Transfers face and vertex attributes from the mesh to the printpoints. Each printpoint is projected to the closest mesh face. It takes directly all the face attributes. It takes the averaged vertex attributes of the face vertices using barycentric coordinates.

Face attributes can be anything. Vertex attributes can only be entities that support multiplication with a scalar. They have been tested to work with scalars and np.arrays.

The reserved attribute names (see 'is_reserved_attribute(attr)') are not passed on to the printpoints.

Parameters:

Name Type Description Default
mesh Mesh

The mesh to transfer attributes from.

required
printpoints PrintPointsCollection

The collection of printpoints to transfer attributes to.

required
Source code in src/compas_slicer/utilities/attributes_transfer.py
def transfer_mesh_attributes_to_printpoints(
    mesh: Mesh,
    printpoints: PrintPointsCollection,
) -> None:
    """
    Transfers face and vertex attributes from the mesh to the printpoints.
    Each printpoint is projected to the closest mesh face. It takes directly all the face attributes.
    It takes the averaged vertex attributes of the face vertices using barycentric coordinates.

    Face attributes can be anything.
    Vertex attributes can only be entities that support multiplication with a scalar. They have been tested to work
    with scalars and np.arrays.

    The reserved attribute names (see 'is_reserved_attribute(attr)') are not passed on to the printpoints.

    Parameters
    ----------
    mesh : Mesh
        The mesh to transfer attributes from.
    printpoints : PrintPointsCollection
        The collection of printpoints to transfer attributes to.

    """
    logger.info('Transferring mesh attributes to the printpoints.')

    all_pts = [ppt.pt for ppt in printpoints.iter_printpoints()]

    closest_fks, projected_pts = pull_pts_to_mesh_faces(mesh, all_pts)

    i = 0
    with progressbar.ProgressBar(max_value=len(all_pts)) as bar:
        for pp in printpoints.iter_printpoints():
            fkey = closest_fks[i]
            proj_pt = projected_pts[i]
            pp.attributes = transfer_mesh_attributes_to_point(mesh, fkey, proj_pt)
            i += 1
            bar.update(i)

is_reserved_attribute

is_reserved_attribute(attr)

Returns True if the attribute name is a reserved, false otherwise.

Source code in src/compas_slicer/utilities/attributes_transfer.py
def is_reserved_attribute(attr: str) -> bool:
    """ Returns True if the attribute name is a reserved, false otherwise. """
    taken_attributes = ['x', 'y', 'z', 'uv',
                        'scalar_field']
    return attr in taken_attributes

transfer_mesh_attributes_to_point

transfer_mesh_attributes_to_point(mesh, fkey, proj_pt)

It projects the point on the closest face of the mesh. Then if finds all the vertex and face attributes of the face and its attributes and transfers them to the point. The vertex attributes are transferred using barycentric coordinates.

Parameters:

Name Type Description Default
mesh Mesh
required
fkey int
required
proj_pt list[float]
required

Returns:

Type Description
dict that contains all the attributes that correspond to the printpoints position
Source code in src/compas_slicer/utilities/attributes_transfer.py
def transfer_mesh_attributes_to_point(mesh: Mesh, fkey: int, proj_pt: list[float]) -> dict[str, Any]:
    """
    It projects the point on the closest face of the mesh. Then if finds
    all the vertex and face attributes of the face and its attributes and transfers them to the point.
    The vertex attributes are transferred using barycentric coordinates.

    Parameters
    ----------
    mesh: compas.datastructures.Mesh
    fkey: face key
    proj_pt: list [x,y,z], point projected on the plane of the face fkey

    Returns
    -------
    dict that contains all the attributes that correspond to the printpoints position
    """

    vs = mesh.face_vertices(fkey)
    bar_coords = barycentric_coordinates(proj_pt, triangle=(mesh.vertex_coordinates(vs[0]),
                                                            mesh.vertex_coordinates(vs[1]),
                                                            mesh.vertex_coordinates(vs[2])))

    # get face attributes
    face_attrs = mesh.face_attributes(fkey)
    keys_to_remove = [attr for attr in face_attrs if is_reserved_attribute(attr)]
    for key in keys_to_remove:
        del face_attrs[key]  # remove from face_attrs dictionary

    # get vertex attributes using barycentric coordinates
    vs = mesh.face_vertices(fkey)
    vertex_attrs: dict[str, Any] = {}
    checked_attrs: list[str] = []
    for attr in mesh.vertex_attributes(vs[0]):
        if not is_reserved_attribute(attr):
            if attr not in checked_attrs:
                check_that_attribute_can_be_multiplied(attr, mesh.vertex_attributes(vs[0])[attr])
                checked_attrs.append(attr)
            vertex_attrs[attr] = 0
            vertex_attrs[attr] += bar_coords[0] * mesh.vertex_attributes(vs[0])[attr]
            vertex_attrs[attr] += bar_coords[1] * mesh.vertex_attributes(vs[1])[attr]
            vertex_attrs[attr] += bar_coords[2] * mesh.vertex_attributes(vs[2])[attr]

    vertex_attrs.update(face_attrs)  # merge two dictionaries
    return vertex_attrs

terminal_command

TerminalCommand class is used to run commands from python as if we are in a shell/cmd

TerminalCommand

TerminalCommand(cmd, cwd=None, env=None)

Creates a new command container. Note that this immediately executes the command synchronously and writes the return values to the corresponding members.

Attributes:

Name Type Description
cmd The command to execute.
cwd The working directory to run the command in.
env The environment the command is run in.
Source code in src/compas_slicer/utilities/terminal_command.py
def __init__(self, cmd, cwd=None, env=None):
    """
    Creates a new command container.
    Note that this immediately executes the command synchronously and writes the
    return values to the corresponding members.

    Attributes
    ----------
    cmd : The command to execute.
    cwd : The working directory to run the command in.
    env : The environment the command is run in.
    """
    process = p.Popen(cmd, stdout=p.PIPE, stderr=p.PIPE, shell=True, cwd=cwd, env=env)
    stdout, stderr = process.communicate()

    self.stdout = stdout.decode('utf8')
    self.stderr = stderr.decode('utf8')

    self.return_code = process.returncode
    process.kill()

utils

remap

remap(input_val, in_from, in_to, out_from, out_to)

Bounded remap from source domain to target domain.

Source code in src/compas_slicer/utilities/utils.py
def remap(input_val: float, in_from: float, in_to: float, out_from: float, out_to: float) -> float:
    """Bounded remap from source domain to target domain."""
    if input_val <= in_from:
        return out_from
    elif input_val >= in_to:
        return out_to
    else:
        return remap_unbound(input_val, in_from, in_to, out_from, out_to)

remap_unbound

remap_unbound(input_val, in_from, in_to, out_from, out_to)

Remap input_val from source domain to target domain (no clamping).

Source code in src/compas_slicer/utilities/utils.py
def remap_unbound(input_val: float, in_from: float, in_to: float, out_from: float, out_to: float) -> float:
    """Remap input_val from source domain to target domain (no clamping)."""
    out_range = out_to - out_from
    in_range = in_to - in_from
    in_val = input_val - in_from
    val = (float(in_val) / in_range) * out_range
    out_val = out_from + val
    return out_val

get_output_directory

get_output_directory(path)

Get or create 'output' directory in the given path.

Parameters:

Name Type Description Default
path str | Path

The path where the 'output' directory will be created.

required

Returns:

Type Description
Path

The path to the 'output' directory.

Source code in src/compas_slicer/utilities/utils.py
def get_output_directory(path: str | Path) -> Path:
    """Get or create 'output' directory in the given path.

    Parameters
    ----------
    path : str | Path
        The path where the 'output' directory will be created.

    Returns
    -------
    Path
        The path to the 'output' directory.

    """
    output_dir = Path(path) / 'output'
    output_dir.mkdir(exist_ok=True)
    return output_dir

get_closest_pt_index

get_closest_pt_index(pt, pts)

Find the index of the closest point to pt in pts.

Parameters:

Name Type Description Default
pt Point | NDArray

Query point.

required
pts list[Point] | NDArray

Point cloud to search.

required

Returns:

Type Description
int

Index of the closest point.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_pt_index(pt: Point | NDArray, pts: list[Point] | NDArray) -> int:
    """Find the index of the closest point to pt in pts.

    Parameters
    ----------
    pt : Point | NDArray
        Query point.
    pts : list[Point] | NDArray
        Point cloud to search.

    Returns
    -------
    int
        Index of the closest point.

    """
    ci: int = closest_point_in_cloud(point=pt, cloud=pts)[2]
    return ci

get_closest_pt

get_closest_pt(pt, pts)

Find the closest point to pt in pts.

Parameters:

Name Type Description Default
pt Point | NDArray

Query point.

required
pts list[Point]

Point cloud to search.

required

Returns:

Type Description
Point

The closest point.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_pt(pt: Point | NDArray, pts: list[Point]) -> Point:
    """Find the closest point to pt in pts.

    Parameters
    ----------
    pt : Point | NDArray
        Query point.
    pts : list[Point]
        Point cloud to search.

    Returns
    -------
    Point
        The closest point.

    """
    ci = closest_point_in_cloud(point=pt, cloud=pts)[2]
    return pts[ci]

pull_pts_to_mesh_faces

pull_pts_to_mesh_faces(mesh, points)

Project points to mesh and find their closest face keys.

Parameters:

Name Type Description Default
mesh Mesh

The mesh to project onto.

required
points list[Point]

Points to project.

required

Returns:

Type Description
tuple[list[int], list[Point]]

Closest face keys and projected points.

Source code in src/compas_slicer/utilities/utils.py
def pull_pts_to_mesh_faces(mesh: Mesh, points: list[Point]) -> tuple[list[int], list[Point]]:
    """Project points to mesh and find their closest face keys.

    Parameters
    ----------
    mesh : Mesh
        The mesh to project onto.
    points : list[Point]
        Points to project.

    Returns
    -------
    tuple[list[int], list[Point]]
        Closest face keys and projected points.

    """
    points_arr = np.array(points, dtype=np.float64).reshape((-1, 3))
    fi_fk = dict(enumerate(mesh.faces()))
    f_centroids = np.array([mesh.face_centroid(fkey) for fkey in mesh.faces()], dtype=np.float64)
    closest_fis = np.argmin(scipy.spatial.distance_matrix(points_arr, f_centroids), axis=1)
    closest_fks = [fi_fk[fi] for fi in closest_fis]
    projected_pts = [closest_point_on_plane(point, mesh.face_plane(fi)) for point, fi in zip(points_arr, closest_fis)]
    return closest_fks, projected_pts

smooth_vectors

smooth_vectors(vectors, strength, iterations)

Smooth vectors iteratively.

Parameters:

Name Type Description Default
vectors list[Vector]

Vectors to smooth.

required
strength float

Smoothing strength (0-1).

required
iterations int

Number of smoothing iterations.

required

Returns:

Type Description
list[Vector]

Smoothed vectors.

Source code in src/compas_slicer/utilities/utils.py
def smooth_vectors(vectors: list[Vector], strength: float, iterations: int) -> list[Vector]:
    """Smooth vectors iteratively.

    Parameters
    ----------
    vectors : list[Vector]
        Vectors to smooth.
    strength : float
        Smoothing strength (0-1).
    iterations : int
        Number of smoothing iterations.

    Returns
    -------
    list[Vector]
        Smoothed vectors.

    """
    for _ in range(iterations):
        for i, n in enumerate(vectors):
            if 0 < i < len(vectors) - 1:
                neighbors_average = (vectors[i - 1] + vectors[i + 1]) * 0.5
            else:
                neighbors_average = n
            vectors[i] = n * (1 - strength) + neighbors_average * strength
    return vectors

save_to_json

save_to_json(data, filepath, name)

Save data to JSON file.

Parameters:

Name Type Description Default
data dict | list

Data to save.

required
filepath str | Path

Directory path.

required
name str

Filename.

required
Source code in src/compas_slicer/utilities/utils.py
def save_to_json(
    data: dict[str, Any] | dict[int, Any] | list[Any], filepath: str | Path, name: str
) -> None:
    """Save data to JSON file.

    Parameters
    ----------
    data : dict | list
        Data to save.
    filepath : str | Path
        Directory path.
    name : str
        Filename.

    """
    filename = Path(filepath) / name
    logger.info(f"Saving to json: {filename}")
    filename.write_text(json.dumps(data, indent=3, sort_keys=True))

load_from_json

load_from_json(filepath, name)

Load data from JSON file.

Parameters:

Name Type Description Default
filepath str | Path

Directory path.

required
name str

Filename.

required

Returns:

Type Description
Any

Loaded data.

Source code in src/compas_slicer/utilities/utils.py
def load_from_json(filepath: str | Path, name: str) -> Any:
    """Load data from JSON file.

    Parameters
    ----------
    filepath : str | Path
        Directory path.
    name : str
        Filename.

    Returns
    -------
    Any
        Loaded data.

    """
    filename = Path(filepath) / name
    data = json.loads(filename.read_text())
    logger.info(f"Loaded json: {filename}")
    return data

is_jsonable

is_jsonable(x)

Return True if x can be JSON-serialized.

Source code in src/compas_slicer/utilities/utils.py
def is_jsonable(x: Any) -> bool:
    """Return True if x can be JSON-serialized."""
    try:
        json.dumps(x)
        return True
    except TypeError:
        return False

get_jsonable_attributes

get_jsonable_attributes(attributes_dict)

Convert attributes dict to JSON-serializable form.

Source code in src/compas_slicer/utilities/utils.py
def get_jsonable_attributes(attributes_dict: dict[str, Any]) -> dict[str, Any]:
    """Convert attributes dict to JSON-serializable form."""
    jsonable_attr: dict[str, Any] = {}
    for attr_key in attributes_dict:
        attr = attributes_dict[attr_key]
        if is_jsonable(attr):
            jsonable_attr[attr_key] = attr
        else:
            if isinstance(attr, np.ndarray):
                jsonable_attr[attr_key] = list(attr)
            else:
                jsonable_attr[attr_key] = 'non serializable attribute'
    return jsonable_attr

save_to_text_file

save_to_text_file(data, filepath, name)

Save text to file.

Parameters:

Name Type Description Default
data str

Text to save.

required
filepath str | Path

Directory path.

required
name str

Filename.

required
Source code in src/compas_slicer/utilities/utils.py
def save_to_text_file(data: str, filepath: str | Path, name: str) -> None:
    """Save text to file.

    Parameters
    ----------
    data : str
        Text to save.
    filepath : str | Path
        Directory path.
    name : str
        Filename.

    """
    filename = Path(filepath) / name
    logger.info(f"Saving to text file: {filename}")
    filename.write_text(data)

check_triangular_mesh

check_triangular_mesh(mesh)

Check if mesh is triangular, raise TypeError if not.

Parameters:

Name Type Description Default
mesh Mesh

The mesh to check.

required

Raises:

Type Description
TypeError

If any face is not a triangle.

Source code in src/compas_slicer/utilities/utils.py
def check_triangular_mesh(mesh: Mesh) -> None:
    """Check if mesh is triangular, raise TypeError if not.

    Parameters
    ----------
    mesh : Mesh
        The mesh to check.

    Raises
    ------
    TypeError
        If any face is not a triangle.

    """
    for f_key in mesh.faces():
        vs = mesh.face_vertices(f_key)
        if len(vs) != 3:
            raise TypeError(f"Found quad at face {f_key}, vertices: {len(vs)}. Only triangular meshes supported.")

get_closest_mesh_vkey_to_pt

get_closest_mesh_vkey_to_pt(mesh, pt)

Find the vertex key closest to the point.

Parameters:

Name Type Description Default
mesh Mesh

The mesh.

required
pt Point

Query point.

required

Returns:

Type Description
int

Closest vertex key.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_mesh_vkey_to_pt(mesh: Mesh, pt: Point) -> int:
    """Find the vertex key closest to the point.

    Parameters
    ----------
    mesh : Mesh
        The mesh.
    pt : Point
        Query point.

    Returns
    -------
    int
        Closest vertex key.

    """
    vertex_tupples = [(v_key, Point(data['x'], data['y'], data['z'])) for v_key, data in mesh.vertices(data=True)]
    vertex_tupples = sorted(vertex_tupples, key=lambda v_tupple: distance_point_point_sqrd(pt, v_tupple[1]))
    closest_vkey: int = vertex_tupples[0][0]
    return closest_vkey

get_closest_mesh_normal_to_pt

get_closest_mesh_normal_to_pt(mesh, pt)

Find the closest vertex normal to the point.

Parameters:

Name Type Description Default
mesh Mesh

The mesh.

required
pt Point

Query point.

required

Returns:

Type Description
Vector

Normal at closest vertex.

Source code in src/compas_slicer/utilities/utils.py
def get_closest_mesh_normal_to_pt(mesh: Mesh, pt: Point) -> Vector:
    """Find the closest vertex normal to the point.

    Parameters
    ----------
    mesh : Mesh
        The mesh.
    pt : Point
        Query point.

    Returns
    -------
    Vector
        Normal at closest vertex.

    """
    closest_vkey = get_closest_mesh_vkey_to_pt(mesh, pt)
    v = mesh.vertex_normal(closest_vkey)
    return Vector(v[0], v[1], v[2])

get_mesh_vertex_coords_with_attribute

get_mesh_vertex_coords_with_attribute(mesh, attr, value)

Get coordinates of vertices where attribute equals value.

Parameters:

Name Type Description Default
mesh Mesh

The mesh.

required
attr str

Attribute name.

required
value Any

Value to match.

required

Returns:

Type Description
list[Point]

Points of matching vertices.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_vertex_coords_with_attribute(mesh: Mesh, attr: str, value: Any) -> list[Point]:
    """Get coordinates of vertices where attribute equals value.

    Parameters
    ----------
    mesh : Mesh
        The mesh.
    attr : str
        Attribute name.
    value : Any
        Value to match.

    Returns
    -------
    list[Point]
        Points of matching vertices.

    """
    pts: list[Point] = []
    for vkey, data in mesh.vertices(data=True):
        if data[attr] == value:
            pts.append(Point(*mesh.vertex_coordinates(vkey)))
    return pts

get_normal_of_path_on_xy_plane

get_normal_of_path_on_xy_plane(k, point, path, mesh)

Find the normal of the curve on xy plane at point with index k.

Parameters:

Name Type Description Default
k int

Index of the point.

required
point Point

The point.

required
path Path

The path containing the point.

required
mesh Mesh

The mesh (fallback for degenerate cases).

required

Returns:

Type Description
Vector

Normal vector.

Source code in src/compas_slicer/utilities/utils.py
def get_normal_of_path_on_xy_plane(k: int, point: Point, path: SlicerPath, mesh: Mesh) -> Vector:
    """Find the normal of the curve on xy plane at point with index k.

    Parameters
    ----------
    k : int
        Index of the point.
    point : Point
        The point.
    path : SlicerPath
        The path containing the point.
    mesh : Mesh
        The mesh (fallback for degenerate cases).

    Returns
    -------
    Vector
        Normal vector.

    """
    # find mesh normal is not really needed in the 2D case of planar slicer
    # instead we only need the normal of the curve based on the neighboring pts
    if (0 < k < len(path.points) - 1) or path.is_closed:
        prev_pt = path.points[k - 1]
        next_pt = path.points[(k + 1) % len(path.points)]
        v1 = np.array(normalize_vector(Vector.from_start_end(prev_pt, point)))
        v2 = np.array(normalize_vector(Vector.from_start_end(point, next_pt)))
        v = (v1 + v2) * 0.5
        normal = [-v[1], v[0], v[2]]  # rotate 90 degrees COUNTER-clockwise on the xy plane

    else:
        if k == 0:
            next_pt = path.points[k + 1]
            v = normalize_vector(Vector.from_start_end(point, next_pt))
            normal = [-v[1], v[0], v[2]]  # rotate 90 degrees COUNTER-clockwise on the xy plane
        else:  # k == len(path.points)-1:
            prev_pt = path.points[k - 1]
            v = normalize_vector(Vector.from_start_end(point, prev_pt))
            normal = [v[1], -v[0], v[2]]  # rotate 90 degrees clockwise on the xy plane

    if length_vector(normal) == 0:
        # When the neighboring elements happen to cancel out, then search for the true normal,
        # and project it on the xy plane for consistency
        normal = get_closest_mesh_normal_to_pt(mesh, point)
        normal = [normal[0], normal[1], 0]

    normal = normalize_vector(normal)
    normal = Vector(*list(normal))
    return normal

get_mesh_cotmatrix

get_mesh_cotmatrix(mesh, fix_boundaries=True)

Get the cotangent Laplacian matrix of the mesh.

Computes L_ij = (cot α_ij + cot β_ij) / 2 for adjacent vertices, with L_ii = -sum_j L_ij (row sum = 0).

Parameters:

Name Type Description Default
mesh Mesh

The mesh (must be triangulated).

required
fix_boundaries bool

If True, zero out rows for boundary vertices.

True

Returns:

Type Description
csr_matrix

Sparse matrix (V x V), cotangent Laplacian.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_cotmatrix(mesh: Mesh, fix_boundaries: bool = True) -> csr_matrix:
    """Get the cotangent Laplacian matrix of the mesh.

    Computes L_ij = (cot α_ij + cot β_ij) / 2 for adjacent vertices,
    with L_ii = -sum_j L_ij (row sum = 0).

    Parameters
    ----------
    mesh : Mesh
        The mesh (must be triangulated).
    fix_boundaries : bool
        If True, zero out rows for boundary vertices.

    Returns
    -------
    csr_matrix
        Sparse matrix (V x V), cotangent Laplacian.

    """
    V, F = mesh.to_vertices_and_faces()
    vertices = np.array(V, dtype=np.float64)
    faces = np.array(F, dtype=np.int32)

    n_vertices = len(vertices)

    # Get cotangent weights for each half-edge
    # For each face, compute cotangents of all three angles
    i0, i1, i2 = faces[:, 0], faces[:, 1], faces[:, 2]
    v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]

    # Edge vectors
    e0 = v2 - v1  # opposite to vertex 0
    e1 = v0 - v2  # opposite to vertex 1
    e2 = v1 - v0  # opposite to vertex 2

    # Cotangent of angle at vertex i = dot(e_j, e_k) / |cross(e_j, e_k)|
    # where e_j and e_k are edges adjacent to vertex i
    def cotangent(a: NDArray, b: NDArray) -> NDArray:
        cross = np.cross(a, b)
        cross_norm = np.linalg.norm(cross, axis=1)
        dot = np.sum(a * b, axis=1)
        # Avoid division by zero
        cross_norm = np.maximum(cross_norm, 1e-10)
        return dot / cross_norm

    # Cotangent at each vertex of each face
    cot0 = cotangent(-e2, e1)  # angle at vertex 0
    cot1 = cotangent(-e0, e2)  # angle at vertex 1
    cot2 = cotangent(-e1, e0)  # angle at vertex 2

    # Build sparse matrix
    # L_ij += 0.5 * cot(angle opposite to edge ij)
    row = np.concatenate([i0, i1, i1, i2, i2, i0])
    col = np.concatenate([i1, i0, i2, i1, i0, i2])
    data = np.concatenate([cot2, cot2, cot0, cot0, cot1, cot1]) * 0.5

    L = csr_matrix((data, (row, col)), shape=(n_vertices, n_vertices))

    # Make symmetric and set diagonal to negative row sum
    L = L + L.T
    L = L - scipy.sparse.diags(np.array(L.sum(axis=1)).flatten())

    if fix_boundaries:
        # Zero out rows for boundary vertices
        boundary_mask = np.zeros(n_vertices, dtype=bool)
        for i, (_vkey, vdata) in enumerate(mesh.vertices(data=True)):
            if vdata.get('boundary', 0) > 0:
                boundary_mask[i] = True

        if np.any(boundary_mask):
            L = L.tolil()
            for i in np.where(boundary_mask)[0]:
                L[i, :] = 0
            L = L.tocsr()

    return L

get_mesh_cotans

get_mesh_cotans(mesh)

Get the cotangent entries of the mesh.

Parameters:

Name Type Description Default
mesh Mesh

The mesh (must be triangulated).

required

Returns:

Type Description
NDArray

F x 3 array of ½*cotangents for corresponding angles. Column i contains cotangent of angle at vertex i of each face.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_cotans(mesh: Mesh) -> NDArray:
    """Get the cotangent entries of the mesh.

    Parameters
    ----------
    mesh : Mesh
        The mesh (must be triangulated).

    Returns
    -------
    NDArray
        F x 3 array of 1/2*cotangents for corresponding angles.
        Column i contains cotangent of angle at vertex i of each face.

    """
    V, F = mesh.to_vertices_and_faces()
    vertices = np.array(V, dtype=np.float64)
    faces = np.array(F, dtype=np.int32)

    i0, i1, i2 = faces[:, 0], faces[:, 1], faces[:, 2]
    v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]

    e0 = v2 - v1
    e1 = v0 - v2
    e2 = v1 - v0

    def cotangent(a: NDArray, b: NDArray) -> NDArray:
        cross = np.cross(a, b)
        cross_norm = np.linalg.norm(cross, axis=1)
        dot = np.sum(a * b, axis=1)
        cross_norm = np.maximum(cross_norm, 1e-10)
        return dot / cross_norm

    cot0 = cotangent(-e2, e1)
    cot1 = cotangent(-e0, e2)
    cot2 = cotangent(-e1, e0)

    return np.column_stack([cot0, cot1, cot2]) * 0.5

get_mesh_massmatrix

get_mesh_massmatrix(mesh)

Get the mass matrix of the mesh (Voronoi area weights).

Parameters:

Name Type Description Default
mesh Mesh

The mesh (must be triangulated).

required

Returns:

Type Description
csr_matrix

Sparse diagonal matrix (V x V), vertex areas.

Source code in src/compas_slicer/utilities/utils.py
def get_mesh_massmatrix(mesh: Mesh) -> csr_matrix:
    """Get the mass matrix of the mesh (Voronoi area weights).

    Parameters
    ----------
    mesh : Mesh
        The mesh (must be triangulated).

    Returns
    -------
    csr_matrix
        Sparse diagonal matrix (V x V), vertex areas.

    """
    V, F = mesh.to_vertices_and_faces()
    vertices = np.array(V, dtype=np.float64)
    faces = np.array(F, dtype=np.int32)

    n_vertices = len(vertices)

    # Compute face areas
    i0, i1, i2 = faces[:, 0], faces[:, 1], faces[:, 2]
    v0, v1, v2 = vertices[i0], vertices[i1], vertices[i2]

    cross = np.cross(v1 - v0, v2 - v0)
    face_areas = 0.5 * np.linalg.norm(cross, axis=1)

    # Distribute 1/3 of each face area to each vertex
    vertex_areas = np.zeros(n_vertices)
    np.add.at(vertex_areas, i0, face_areas / 3)
    np.add.at(vertex_areas, i1, face_areas / 3)
    np.add.at(vertex_areas, i2, face_areas / 3)

    return scipy.sparse.diags(vertex_areas)

point_list_to_dict

point_list_to_dict(pts_list)

Convert list of points/vectors to dict for JSON.

Parameters:

Name Type Description Default
pts_list list[Point | Vector]

List of points or vectors.

required

Returns:

Type Description
dict[int, list[float]]

Dict mapping index to [x, y, z].

Source code in src/compas_slicer/utilities/utils.py
def point_list_to_dict(pts_list: list[Point | Vector]) -> dict[int, list[float]]:
    """Convert list of points/vectors to dict for JSON.

    Parameters
    ----------
    pts_list : list[Point | Vector]
        List of points or vectors.

    Returns
    -------
    dict[int, list[float]]
        Dict mapping index to [x, y, z].

    """
    data: dict[int, list[float]] = {}
    for i in range(len(pts_list)):
        data[i] = list(pts_list[i])
    return data

point_list_from_dict

point_list_from_dict(data)

Convert dict of points to list of [x, y, z].

Parameters:

Name Type Description Default
data dict[Any, list[float]]

Dict mapping keys to [x, y, z].

required

Returns:

Type Description
list[list[float]]

List of [x, y, z] coordinates.

Source code in src/compas_slicer/utilities/utils.py
def point_list_from_dict(data: dict[Any, list[float]]) -> list[list[float]]:
    """Convert dict of points to list of [x, y, z].

    Parameters
    ----------
    data : dict[Any, list[float]]
        Dict mapping keys to [x, y, z].

    Returns
    -------
    list[list[float]]
        List of [x, y, z] coordinates.

    """
    return [[data[i][0], data[i][1], data[i][2]] for i in data]

flattened_list_of_dictionary

flattened_list_of_dictionary(dictionary)

Flatten dictionary values into a single list.

Parameters:

Name Type Description Default
dictionary dict[Any, list[Any]]

Dictionary with list values.

required

Returns:

Type Description
list[Any]

Flattened list.

Source code in src/compas_slicer/utilities/utils.py
def flattened_list_of_dictionary(dictionary: dict[Any, list[Any]]) -> list[Any]:
    """Flatten dictionary values into a single list.

    Parameters
    ----------
    dictionary : dict[Any, list[Any]]
        Dictionary with list values.

    Returns
    -------
    list[Any]
        Flattened list.

    """
    flattened_list: list[Any] = []
    for key in dictionary:
        for item in dictionary[key]:
            flattened_list.append(item)
    return flattened_list

get_dict_key_from_value

get_dict_key_from_value(dictionary, val)

Return the key of a dictionary that stores the value.

Parameters:

Name Type Description Default
dictionary dict

The dictionary to search.

required
val Any

Value to find.

required

Returns:

Type Description
Any | None

The key, or None if not found.

Source code in src/compas_slicer/utilities/utils.py
def get_dict_key_from_value(dictionary: dict[Any, Any], val: Any) -> Any | None:
    """Return the key of a dictionary that stores the value.

    Parameters
    ----------
    dictionary : dict
        The dictionary to search.
    val : Any
        Value to find.

    Returns
    -------
    Any | None
        The key, or None if not found.

    """
    for key in dictionary:
        value = dictionary[key]
        if val == value:
            return key
    return None

find_next_printpoint

find_next_printpoint(printpoints, i, j, k)

Returns the next printpoint from the current printpoint if it exists, otherwise returns None.

Parameters:

Name Type Description Default
printpoints PrintPointsCollection

The collection of printpoints.

required
i int

Layer index.

required
j int

Path index.

required
k int

Printpoint index within the path.

required

Returns:

Type Description
PrintPoint | None

The next printpoint or None if at the end.

Source code in src/compas_slicer/utilities/utils.py
def find_next_printpoint(
    printpoints: PrintPointsCollection, i: int, j: int, k: int
) -> PrintPoint | None:
    """
    Returns the next printpoint from the current printpoint if it exists, otherwise returns None.

    Parameters
    ----------
    printpoints : PrintPointsCollection
        The collection of printpoints.
    i : int
        Layer index.
    j : int
        Path index.
    k : int
        Printpoint index within the path.

    Returns
    -------
    PrintPoint | None
        The next printpoint or None if at the end.

    """
    next_ppt = None
    if k < len(printpoints[i][j]) - 1:  # If there are more ppts in the current path
        next_ppt = printpoints[i][j][k + 1]
    else:
        if j < len(printpoints[i]) - 1:  # Otherwise take the next path
            next_ppt = printpoints[i][j + 1][0]
        else:
            if i < len(printpoints) - 1:  # Otherwise take the next layer
                next_ppt = printpoints[i + 1][0][0]
    return next_ppt

find_previous_printpoint

find_previous_printpoint(printpoints, i, j, k)

Returns the previous printpoint from the current printpoint if it exists, otherwise returns None.

Parameters:

Name Type Description Default
printpoints PrintPointsCollection

The collection of printpoints.

required
i int

Layer index.

required
j int

Path index.

required
k int

Printpoint index within the path.

required

Returns:

Type Description
PrintPoint | None

The previous printpoint or None if at the start.

Source code in src/compas_slicer/utilities/utils.py
def find_previous_printpoint(
    printpoints: PrintPointsCollection, i: int, j: int, k: int
) -> PrintPoint | None:
    """
    Returns the previous printpoint from the current printpoint if it exists, otherwise returns None.

    Parameters
    ----------
    printpoints : PrintPointsCollection
        The collection of printpoints.
    i : int
        Layer index.
    j : int
        Path index.
    k : int
        Printpoint index within the path.

    Returns
    -------
    PrintPoint | None
        The previous printpoint or None if at the start.

    """
    prev_ppt = None
    if k > 0:  # If not the first point in a path
        prev_ppt = printpoints[i][j][k - 1]
    else:
        if j > 0:  # Otherwise take the last point of the previous path
            prev_ppt = printpoints[i][j - 1][-1]
        else:
            if i > 0:  # Otherwise take the last path of the previous layer
                prev_ppt = printpoints[i - 1][-1][-1]
    return prev_ppt

interrupt

interrupt()

Interrupts the flow of the code while it is running. It asks for the user to press a enter to continue or abort.

Source code in src/compas_slicer/utilities/utils.py
def interrupt() -> None:
    """
    Interrupts the flow of the code while it is running.
    It asks for the user to press a enter to continue or abort.
    """
    value = input("Press enter to continue, Press 1 to abort ")
    if isinstance(value, str) and value == '1':
        raise ValueError("Aborted")

get_all_files_with_name

get_all_files_with_name(startswith, endswith, DATA_PATH)

Finds all the filenames in the DATA_PATH that start and end with the provided strings

Parameters:

Name Type Description Default
startswith str
required
endswith str
required
DATA_PATH str | Path
required

Returns:

Type Description
list[str]

All the filenames

Source code in src/compas_slicer/utilities/utils.py
def get_all_files_with_name(
    startswith: str, endswith: str, DATA_PATH: str | Path
) -> list[str]:
    """
    Finds all the filenames in the DATA_PATH that start and end with the provided strings

    Parameters
    ----------
    startswith: str
    endswith: str
    DATA_PATH: str | Path

    Returns
    ----------
    list[str]
        All the filenames
    """
    files = [f.name for f in Path(DATA_PATH).iterdir()
             if f.name.startswith(startswith) and f.name.endswith(endswith)]
    logger.info(f'Reloading: {files}')
    return files

check_package_is_installed

check_package_is_installed(package_name)

Throws an error if igl python bindings are not installed in the current environment.

Source code in src/compas_slicer/utilities/utils.py
def check_package_is_installed(package_name: str) -> None:
    """ Throws an error if igl python bindings are not installed in the current environment. """
    packages = TerminalCommand('conda list').get_split_output_strings()
    if package_name not in packages:
        raise PluginNotInstalledError(" ATTENTION! Package : " + package_name +
                                      " is missing! Please follow installation guide to install it.")