Source code for compas_slicer.utilities.utils


import os
import json
import logging
from compas.geometry import Point, distance_point_point_sqrd, normalize_vector
from compas.geometry import Vector, length_vector, closest_point_in_cloud, closest_point_on_plane
import matplotlib.pyplot as plt
import networkx as nx
import numpy as np
import scipy

logger = logging.getLogger('logger')

__all__ = ['remap',
           'remap_unbound',
           'get_output_directory',
           'save_to_json',
           'load_from_json',
           'is_jsonable',
           'get_jsonable_attributes',
           'save_to_text_file',
           'flattened_list_of_dictionary',
           'interrupt',
           'point_list_to_dict',
           'point_list_from_dict',
           'get_closest_mesh_vkey_to_pt',
           'get_mesh_cotmatrix_igl',
           'get_mesh_cotans_igl',
           'get_closest_pt_index',
           'get_closest_pt',
           'pull_pts_to_mesh_faces',
           'plot_networkx_graph',
           'get_mesh_vertex_coords_with_attribute',
           'get_dict_key_from_value',
           'find_next_printpoint',
           'find_previous_printpoint',
           'smooth_vectors',
           'get_normal_of_path_on_xy_plane',
           'get_all_files_with_name',
           'get_closest_mesh_normal_to_pt']


def remap(input_val, in_from, in_to, out_from, out_to):
    """ Bounded remap. """
    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)


def remap_unbound(input_val, in_from, in_to, out_from, out_to):
    """
    Remaps input_val from source domain to target domain.
    No clamping is performed, the result can be outside of the target domain
    if the input is outside of the source domain.
    """
    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


def get_output_directory(path):
    """
    Checks if a directory with the name 'output' exists in the path. If not it creates it.

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

    Returns
    ----------
    str
        The path to the new (or already existing) 'output' directory
    """
    output_dir = os.path.join(path, 'output')
    if not os.path.exists(output_dir):
        os.mkdir(output_dir)
    return output_dir


[docs]def get_closest_pt_index(pt, pts): """ Finds the index of the closest point of 'pt' in the point cloud 'pts'. Parameters ---------- pt: compas.geometry.Point3d pts: list, compas.geometry.Point3d Returns ---------- int The index of the closest point """ ci = closest_point_in_cloud(point=pt, cloud=pts)[2] # distances = [distance_point_point_sqrd(p, pt) for p in pts] # ci = distances.index(min(distances)) return ci
[docs]def get_closest_pt(pt, pts): """ Finds the closest point of 'pt' in the point cloud 'pts'. Parameters ---------- pt: :class: 'compas.geometry.Point' pts: list, :class: 'compas.geometry.Point3d' Returns ---------- compas.geometry.Point3d The closest point """ ci = closest_point_in_cloud(point=pt, cloud=pts)[2] return pts[ci]
def pull_pts_to_mesh_faces(mesh, points): """ Very fast method for projecting a list of points on a mesh, and finding their closest face keys. Parameters ---------- mesh: :class: compas.datastructures.Mesh points: list, compas.geometry.Point Returns ------- closest_fks: a list of the closest face keys projected_pts: a list of the projected points on the mesh """ points = np.array(points, dtype=np.float64).reshape((-1, 3)) fi_fk = {index: fkey for index, fkey in 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, 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, closest_fis)] return closest_fks, projected_pts
[docs]def smooth_vectors(vectors, strength, iterations): """ Smooths the vector iteratively, with the given number of iterations and strength per iteration Parameters ---------- vectors: list, :class: 'compas.geometry.Vector' strength: float iterations: int Returns ---------- list, :class: 'compas.geometry.Vector3d' The smoothened 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
####################################### # json
[docs]def save_to_json(data, filepath, name): """ Save the provided data to json on the filepath, with the given name Parameters ---------- data: dict_or_list filepath: str name: str """ filename = os.path.join(filepath, name) logger.info("Saving to json: " + filename) with open(filename, 'w') as f: f.write(json.dumps(data, indent=3, sort_keys=True))
[docs]def load_from_json(filepath, name): """ Loads json from the filepath Parameters ---------- filepath: str name: str """ filename = os.path.join(filepath, name) with open(filename, 'r') as f: data = json.load(f) logger.info("Loaded json: " + filename) return data
def is_jsonable(x): """ Returns True if x can be json-serialized, False otherwise. """ try: json.dumps(x) return True except TypeError: return False def get_jsonable_attributes(attributes_dict): jsonable_attr = {} 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 ####################################### # text file def save_to_text_file(data, filepath, name): """ Save the provided text on the filepath, with the given name Parameters ---------- data: str filepath: str name: str """ filename = os.path.join(filepath, name) logger.info("Saving to text file: " + filename) with open(filename, 'w') as f: f.write(data) ####################################### # mesh utils def check_triangular_mesh(mesh): """ Checks if the mesh is triangular. If not, then it raises an error Parameters ---------- mesh: :class: 'compas.datastructures.Mesh' """ for f_key in mesh.faces(): vs = mesh.face_vertices(f_key) if len(vs) != 3: raise TypeError("Found a quad at face key: " + str(f_key) + " ,number of face vertices:" + str( len(vs)) + ". \nOnly triangular meshes supported.") def get_closest_mesh_vkey_to_pt(mesh, pt): """ Finds the vertex key that is the closest to the point. Parameters ---------- mesh: :class: 'compas.datastructures.Mesh' pt: :class: 'compas.geometry.Point' Returns ---------- int the closest vertex key """ # cloud = [Point(data['x'], data['y'], data['z']) for v_key, data in mesh.vertices(data=True)] # closest_index = compas.geometry.closest_point_in_cloud(pt, cloud)[2] 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 = vertex_tupples[0][0] return closest_vkey
[docs]def get_closest_mesh_normal_to_pt(mesh, pt): """ Finds the closest vertex normal to the point. Parameters ---------- mesh: :class: 'compas.datastructures.Mesh' pt: :class: 'compas.geometry.Point' Returns ---------- :class: 'compas.geometry.Vector' The closest normal of the mesh. """ closest_vkey = get_closest_mesh_vkey_to_pt(mesh, pt) v = mesh.vertex_normal(closest_vkey) return Vector(v[0], v[1], v[2])
[docs]def get_mesh_vertex_coords_with_attribute(mesh, attr, value): """ Finds the coordinates of all the vertices that have an attribute with key=attr that equals the value. Parameters ---------- mesh: :class: 'compas.datastructures.Mesh' attr: str value: anything that can be stored into a dictionary Returns ---------- list, :class: 'compas.geometry.Point' the closest vertex key """ pts = [] for vkey, data in mesh.vertices(data=True): if data[attr] == value: pts.append(Point(*mesh.vertex_coordinates(vkey))) return pts
[docs]def get_normal_of_path_on_xy_plane(k, point, path, mesh): """ Finds the normal of the curve that lies on the xy plane at the point with index k Parameters ---------- k: int, index of the point point: :class: 'compas.geometry.Point' path: :class: 'compas_slicer.geometry.Path' mesh: :class: 'compas.datastructures.Mesh' Returns ---------- :class: 'compas.geometry.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
####################################### # igl utils def get_mesh_cotmatrix_igl(mesh, fix_boundaries=True): """ Gets the laplace operator of the mesh Parameters ---------- mesh: :class: 'compas.datastructures.Mesh' fix_boundaries : bool Returns ---------- :class: 'scipy.sparse.csr_matrix' sparse matrix (dimensions: #V x #V), laplace operator, each row i corresponding to v(i, :) """ import igl v, f = mesh.to_vertices_and_faces() C = igl.cotmatrix(np.array(v), np.array(f)) if fix_boundaries: # fix boundaries by putting the corresponding columns of the sparse matrix to 0 C_dense = C.toarray() for i, (vkey, data) in enumerate(mesh.vertices(data=True)): if data['boundary'] > 0: C_dense[i][:] = np.zeros(len(v)) C = scipy.sparse.csr_matrix(C_dense) return C def get_mesh_cotans_igl(mesh): """ Gets the cotangent entries of the mesh Parameters ---------- mesh: :class: 'compas.datastructures.Mesh' Returns ---------- :class: 'np.array' Dimensions: F by 3 list of 1/2*cotangents corresponding angles """ import igl v, f = mesh.to_vertices_and_faces() return igl.cotmatrix_entries(np.array(v), np.array(f)) ####################################### # networkx graph
[docs]def plot_networkx_graph(G): """ Plots the graph G Parameters ---------- G: networkx.Graph """ plt.subplot(121) nx.draw(G, with_labels=True, font_weight='bold', node_color=range(len(list(G.nodes())))) plt.show()
####################################### # dict utils
[docs]def point_list_to_dict(pts_list): """ Turns a list of compas.geometry.Point into a dictionary, so that it can be saved to Json. Works identically for 3D vectors. Parameters ---------- pts_list: list, :class:`compas.geometry.Point` / :class:`compas.geometry.Vector` Returns ---------- dict: The dictionary of pts in the form { key=index : [x,y,z] } """ data = {} for i in range(len(pts_list)): data[i] = list(pts_list[i]) return data
def point_list_from_dict(data): """ Turns a dictionary of pts to a list of Compas.geometry.Point. Works identically for 3D vectors. Parameters ---------- dict: The dictionary of pts in the form { key=index : [x,y,z] } Returns ---------- 2D list, [[x1, y1, z1], ... , [xn, yn, zn]] """ return [[data[i][0], data[i][1], data[i][2]] for i in data] # --- Flattened list of dictionary
[docs]def flattened_list_of_dictionary(dictionary): """ Turns the dictionary into a flat list Parameters ---------- dictionary: dict Returns ---------- list """ flattened_list = [] for key in dictionary: [flattened_list.append(item) for item in dictionary[key]] return flattened_list
[docs]def get_dict_key_from_value(dictionary, val): """ Return the key of a dictionary that stores the val Parameters ---------- dictionary: dict val: anything that can be stored in a dictionary """ for key in dictionary: value = dictionary[key] if val == value: return key return "key doesn't exist"
def find_next_printpoint(pp_dict, i, j, k): """ Returns the next printpoint from the current printpoint if it exists, otherwise returns None. """ next_ppt = None layer_key, path_key = 'layer_%d' % i, 'path_%d' % j if k < len(pp_dict[layer_key][path_key]) - 1: # If there are more ppts in the current path, then take the next ppt next_ppt = pp_dict[layer_key][path_key][k + 1] else: if j < len(pp_dict[layer_key]) - 1: # Otherwise take the next path if there are more paths in the current layer next_ppt = pp_dict[layer_key]['path_%d' % (j + 1)][0] else: if i < len(pp_dict) - 1: # Otherwise take the next layer if there are more layers in the current slicer next_ppt = pp_dict['layer_%d' % (i + 1)]['path_0'][0] return next_ppt def find_previous_printpoint(pp_dict, layer_key, path_key, i, j, k): """ Returns the previous printpoint from the current printpoint if it exists, otherwise returns None. """ prev_ppt = None if k > 0: # If not the first point in a path, take the previous point in the path prev_ppt = pp_dict[layer_key][path_key][k - 1] else: if j > 0: # Otherwise take the last point of the previous path, if there are more paths in the current layer prev_ppt = pp_dict[layer_key]['path_%d' % (j - 1)][-1] else: if i > 0: # Otherwise take the last path of the previous layer if there are more layers in the current slicer last_path_key = len(pp_dict[layer_key]) - 1 prev_ppt = pp_dict['layer_%d' % (i - 1)]['path_%d' % (last_path_key)][-1] return prev_ppt ####################################### # control flow
[docs]def interrupt(): """ 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 ") print("") if isinstance(value, str): if value == '1': raise ValueError("Aborted")
####################################### # load all files with name def 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 ---------- startswith: str endswith: str DATA_PATH: str Returns ---------- list, str All the filenames """ files = [] for file in os.listdir(DATA_PATH): if file.startswith(startswith) and file.endswith(endswith): files.append(file) print('') logger.info('Reloading : ' + str(files)) return files if __name__ == "__main__": pass