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