diff --git a/src/compas/geometry/_intersections/mesh_intersections.py b/src/compas/geometry/_intersections/mesh_intersections.py new file mode 100644 index 000000000000..646344cfd7a8 --- /dev/null +++ b/src/compas/geometry/_intersections/mesh_intersections.py @@ -0,0 +1,96 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.geometry import Point +from compas.geometry import length_vector +from compas.geometry import subtract_vectors +from compas.geometry import intersection_line_triangle +from compas.geometry import intersection_segment_plane + +__all__ = [ + 'intersection_mesh_line', + 'intersection_mesh_plane', + 'mesh_vertices_to_points', +] + + +def intersection_mesh_line(mesh, line): + """Compute intersection between mesh faces and line. After one single intersection, stops searching for more. + + Parameters + ---------- + mesh : compas.datastructures.Mesh + line : compas.geometry.Line + + Returns + ------- + Point : compas.geometry.Point + """ + for fkey in list(mesh.faces()): + vertex_keys = mesh.face_vertices(fkey) + if not vertex_keys: + continue + vertices = [mesh.vertex_attributes(vkey, 'xyz') for vkey in vertex_keys] + if len(vertex_keys) not in (3, 4): + continue + + triangle = [vertices[0], vertices[1], vertices[2]] + intersection = intersection_line_triangle(line, triangle) + if intersection: + return Point(intersection[0], intersection[1], intersection[2]) + + if len(vertex_keys) == 4: + triangle_2 = [vertices[2], vertices[3], vertices[0]] + intersection_2 = intersection_line_triangle(line, triangle_2) + if intersection_2: + return Point(intersection_2[0], intersection_2[1], intersection_2[2]) + + return None + + +def intersection_mesh_plane(mesh, plane, tol=0.0001): + """Calculate the keys of the points of the intersection of a mesh with a plane + + Parameters + ---------- + mesh : compas.datastructures.Mesh + plane : compas.geometry.Plane + + Returns + ------- + intersections: list of points as keys from mesh + """ + intersections = [] + for u, v in list(mesh.edges()): + a = mesh.vertex_attributes(u, 'xyz') + b = mesh.vertex_attributes(v, 'xyz') + intersection = intersection_segment_plane((a, b), plane) + if not intersection: + continue + len_a_inters = length_vector(subtract_vectors(intersection, a)) + len_a_b = length_vector(subtract_vectors(b, a)) + t = len_a_inters / len_a_b + if t >= 1.0: + t = 1 - tol + elif t <= 0.0: + t = tol + intersection_key = mesh.split_edge(u, v, t=t, allow_boundary=True) + intersections.append(intersection_key) + return intersections + + +def mesh_vertices_to_points(mesh, v_keys): + """Compute compas points from vertex keys from specific mesh + Returns list of compas points from a list of indexes of the vertexes of a mesh + + Parameters + ---------- + mesh : compas.datastructures.Mesh + v_keys : list of vertex indexes of a mesh + + Returns + ------- + list of compas.geometry.Point + """ + return [Point(*mesh.vertex_attributes(v_key, 'xyz')) for v_key in v_keys] diff --git a/src/compas/geometry/_primitives/line.py b/src/compas/geometry/_primitives/line.py index ebf9a6e1d36e..1d5aa5a32c6d 100644 --- a/src/compas/geometry/_primitives/line.py +++ b/src/compas/geometry/_primitives/line.py @@ -5,7 +5,6 @@ from compas.geometry._primitives import Primitive from compas.geometry._primitives import Point - __all__ = ['Line'] @@ -337,11 +336,37 @@ def transformed(self, T): line.transform(T) return line + def divide_by_count(self, number=10, include_ends=False): + """Return list of points from dividing the line by specific number of divisions + + Parameters + ---------- + number : integer + number of divisions + includeEnds : boolean + True if including start and end point in division points + False if not including start and end point in division points + + Returns + ------- + list of :class:`compas.geometry.Point` + Point as sequence of values xyz + Example + -------- + >>> line = Line([0.0, 0.0, 0.0], [5.0 ,0.0, 0.0]) + >>> line.divide_by_count(5, True) + [Point(0.000, 0.000, 0.000), Point(1.000, 0.000, 0.000), Point(2.000, 0.000, 0.000), Point(3.000, 0.000, 0.000), Point(4.000, 0.000, 0.000), Point(5.000, 0.000, 0.000)] + """ + if include_ends: + return [self.point(i * float(1.0 / number)) for i in range(int(number) + 1)] + else: + return [self.point(i * float(1.0 / number)) for i in range(int(number)) if i != 0] # ============================================================================== # Main # ============================================================================== + if __name__ == '__main__': import doctest diff --git a/src/compas/geometry/_primitives/polyline.py b/src/compas/geometry/_primitives/polyline.py index 7ba57703a12a..fac053ac1768 100644 --- a/src/compas/geometry/_primitives/polyline.py +++ b/src/compas/geometry/_primitives/polyline.py @@ -319,11 +319,112 @@ def transformed(self, T): polyline.transform(T) return polyline + def shorten(self, start_distance=0, end_distance=0): + """Return a new polyline which is shorter than the original in one end side, other or both by a given distance. + + Parameters + ---------- + start_distance : float. + distance to shorten from the starting point of the polyline + end_distance : float. + distance to shorten from the ending point of the polyline + + Returns + ------- + :class:`compas.geometry.Polyline` + The transformed copy. + """ + if start_distance != 0 or end_distance != 0: + points = [] + acum_length = 0 + switch = True + for i, line in enumerate(self.lines): + acum_length += line.length + if acum_length < start_distance: + continue + elif acum_length > start_distance and switch: + if start_distance == 0: + points.append(line.start) + else: + points.append(self.point(start_distance/self.length)) + switch = False + else: + points.append(line.start) + if end_distance == 0: + if i == len(self.lines)-1: + points.append(line.end) + else: + if acum_length >= (self.length - end_distance): + points.append(self.point(1-(end_distance/self.length))) + break + return points + return self + + def rebuild(self, number=20): + """Reconstruct a polyline with evenly spaced points based on a number of interpolations + Returns new rebuilt polyline + + Parameters + ---------- + number : integer. + number of points for the amount of definition of the polyline + + Returns + ------- + :class: 'compas.geometry.Polyline' + the rebuilt copy + """ + rebuilt_polyline = self.copy() + points = [self.point(i * float(1.0 / number)) for i in range(number)] + points.append(self.point(1)) + rebuilt_polyline.points = [Point(x, y, z) for x, y, z in points] + return rebuilt_polyline + + def divide_by_count(self, number=10, include_ends=False): + """Divide a polyline by count. Returns list of Points from the division + + Parameters + ---------- + number : integer. + number of divisions + includeEnds : boolean + True if including start and ending points. + False if not including start and ending points. + + Returns + ------- + list of :class: 'compas.geometry.Point' + """ + points = [self.point(i * float(1.0 / number)) for i in range(number)] + if include_ends: + points.append(self.point(1)) + else: + points.pop(0) + return points + + def tween(self, polyline_two, number=50): + """Create an average polyline between two polylines interpolating their points + + Parameters + ---------- + polyline_two : compas.geometry.Polyline + polyline to create the tween polyline + number : number of points of the tween polyline + + Returns + ------- + list of :class: 'compas.geometry.Point' + """ + rebuilt_polyline_one = self.rebuild(number) + rebuilt_polyline_two = polyline_two.rebuild(number) + lines = [Line(point_one, point_two) for point_one, point_two in zip(rebuilt_polyline_one, rebuilt_polyline_two)] + return [line.midpoint for line in lines] # ============================================================================== # Main # ============================================================================== + if __name__ == '__main__': import doctest diff --git a/src/compas/geometry/_transformations/extend.py b/src/compas/geometry/_transformations/extend.py new file mode 100644 index 000000000000..bf120f992ae8 --- /dev/null +++ b/src/compas/geometry/_transformations/extend.py @@ -0,0 +1,94 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.geometry import Translation +from compas.geometry import Polyline +from compas.geometry import Vector + +__all__ = [ + 'extend_line', + 'extend_polyline', +] + + +def extend_line(line, start_extension=0, end_extension=0): + """Extend the given line from one end or the other, or both, depending on the given values + + Parameters + ---------- + line : tuple + Two points defining the line. + start_extension : float + The extension distance at the start of the line as float. + end_extension : float + The extension distance at the end of the line as float. + + Returns + ------- + extended line : tuple + Two points defining the extended line. + + Examples + -------- + >>> line = Line([0.0, 0.0, 0.0], [1.0, 0.0, 0.0]) + >>> extended_line = extend_line(line, 1, 1) + Line([-1.0, 0.0, 0.0], [2.0, 0.0, 0.0]) + """ + def calculate_translation(line, distance): + vector = line.direction.copy() + vector.scale(distance) + return Translation(vector) + + if start_extension != 0: + translation = calculate_translation(line, -start_extension) + line.start.transform(translation) + if end_extension != 0: + translation = calculate_translation(line, end_extension) + line.end.transform(translation) + + return line + + +def extend_polyline(polyline, start_extension=0, end_extension=0): + """Extend a polyline by line from the vectors on segments at extreme sides + + Parameters + ---------- + polyline : list + list of points defining the polyline. + start_extension : float + The extension distance at the start of the polyline as float. + end_extension : float + The extension distance at the end of the polyline as float. + + Returns + ------- + extended polyline : compas.geometry.Polyline(points) + + Examples + -------- + >>> polyline = Polyline([0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 1.0, 0.0], [3.0, 1.0, 0.0], [4.0, 0.0, 0.0], [5.0, 0.0, 0.0]) + >>> extended_polyline = extend_polyline(polyline, 1, 1) + Polyline([-1.0, 0.0, 0.0], [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [2.0, 1.0, 0.0], [3.0, 1.0, 0.0], [4.0, 0.0, 0.0], [5.0, 0.0, 0.0], [6.0, 0.0, 0.0]) + """ + def move_point(point, vector, distance): + vector.unitize() + vector.scale(distance) + move = Translation(vector) + return point.transformed(move) + + points = polyline.points + if start_extension != 0: + point_start = polyline.points[0] + vec = Vector.from_start_end(polyline.points[1], point_start) + new_point_start = move_point(point_start, vec, start_extension) + points.insert(0, new_point_start) + + if end_extension != 0: + point_end = polyline.points[-1] + vec_end = Vector.from_start_end(polyline.points[-2], point_end) + new_point_end = move_point(point_end, vec_end, end_extension) + points.append(new_point_end) + + return Polyline(points) diff --git a/src/compas/geometry/splits/__init__.py b/src/compas/geometry/splits/__init__.py new file mode 100644 index 000000000000..327f2c7d1a57 --- /dev/null +++ b/src/compas/geometry/splits/__init__.py @@ -0,0 +1,5 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +__all__ = [name for name in dir() if not name.startswith('_')] diff --git a/src/compas/geometry/splits/splits.py b/src/compas/geometry/splits/splits.py new file mode 100644 index 000000000000..8b91ec3793c2 --- /dev/null +++ b/src/compas/geometry/splits/splits.py @@ -0,0 +1,129 @@ +from __future__ import print_function +from __future__ import absolute_import +from __future__ import division + +from compas.utilities import pairwise + +from compas.datastructures import Mesh +from compas.geometry import Polyline +from compas.geometry import intersection_segment_plane +from compas.geometry._intersections.mesh_intersections import intersection_mesh_plane +from compas.geometry import subtract_vectors +from compas.geometry import dot_vectors + +__all__ = [ + 'split_polyline_plane', + 'split_mesh_plane', +] + + +def split_polyline_plane(polyline, plane): + """Split a polyline by a plane, returns a list of polylines, if there has been intersections + Returns list of the splitted polylines + + Parameters + ---------- + polyline : compas.geometry.Polyline + plane : compas.geometry.Plane + + Returns + ------- + list of :class: 'compas.geometry.Polyline' + """ + points_from_split_polylines = [] + sublist = [] + for i, segment in enumerate(pairwise(polyline)): + sublist.append(segment.start) + intersection = intersection_segment_plane(segment, plane) + if intersection: + sublist.append(intersection) + points_from_split_polylines.append(sublist) + sublist = [] + sublist.append(intersection) + if i == len(pairwise(polyline)) - 1: + sublist.append(segment.end) + points_from_split_polylines.append(sublist) + + return [Polyline(sublist) for sublist in points_from_split_polylines] + + +def _mesh_from_split(mesh, v_keys, f_keys, intersections, open=True): + """Return a mesh from the positive or negative verts, faces and the intersection verts of a splitted mesh + + Parameters + ---------- + v_keys : list of integers + list of keys from vertices of the mesh as integers + f_keys : list of faces + list of face keys(list of vertices forming the face) + intersections : list of integers + list of keys from the intersection of a mesh with a plane + open : boolean + True if the result remains open, False if the resulting mesh is closed + + Returns + ------- + compas.datastructures.Mesh + """ + vertices = {key: mesh.vertex_coordinates(key) for key in v_keys + intersections} + faces = [mesh.face_vertices(f_key) for f_key in f_keys] + final_mesh = Mesh.from_vertices_and_faces(vertices, faces) + if not open: + final_mesh.add_face(final_mesh.vertices_on_boundary(True)) + return final_mesh + + +def split_mesh_plane(mesh, plane, open=True): + """Calculate all the intersections between edges of the mesh and cutting plane, + and splits every mesh edge at the intersection point, if it exists. + Returns a list of the resulting splitted meshes. + + Parameters + ---------- + mesh : compas.datastructures.Mesh + plane : compas.geometry.Plane + + Returns + ------- + splitted_meshes : list of compas.datastructures.Mesh + """ + + intersections = intersection_mesh_plane(mesh, plane) + + if len(intersections) < 3: + return None + + for f_key in list(mesh.faces()): + split = [v_key for v_key in mesh.face_vertices(f_key) if v_key in intersections] + if len(split) == 2: + mesh.split_face(f_key, split[0], split[1]) + + positive_vertices = [] + negative_vertices = [] + for v_key in mesh.vertices(): + if v_key in intersections: + continue + vert_a = mesh.vertex_attributes(v_key, 'xyz') + ori_vert_a = subtract_vectors(vert_a, plane.point) + similarity = dot_vectors(plane.normal, ori_vert_a) + if similarity > 0.0: + positive_vertices.append(v_key) + elif similarity < 0.0: + negative_vertices.append(v_key) + + positive_faces = [] + for key in positive_vertices: + positive_faces += mesh.vertex_faces(key) + positive_faces = list(set(positive_faces)) + + negative_faces = [] + for key in negative_vertices: + negative_faces += mesh.vertex_faces(key) + negative_faces = list(set(negative_faces)) + + positive_mesh = _mesh_from_split(mesh, positive_vertices, positive_faces, intersections, open) + negative_mesh = _mesh_from_split(mesh, negative_vertices, negative_faces, intersections, open) + + splitted_meshes = [positive_mesh, negative_mesh] + return splitted_meshes + \ No newline at end of file