Source code for design3d.faces

"""
Surfaces & faces.

"""

import math
import warnings
from itertools import chain, product
from typing import List
import matplotlib.pyplot as plt
import numpy as np
import triangle as triangle_lib

import design3d.core
from design3d.core import EdgeStyle
import design3d.core_compiled
import design3d.display as d3dd
import design3d.edges as d3de
import design3d.curves as design3d_curves
import design3d.geometry
import design3d.grid
from design3d import surfaces
from design3d.utils.parametric import update_face_grid_points_with_inner_polygons
import design3d.wires

warnings.simplefilter("once")


[docs] def octree_decomposition(bbox, faces): """Decomposes a list of faces into eight Bounding boxes subdivided boxes.""" decomposition = {octant: [] for octant in bbox.octree()} for face in faces: center = face.bounding_box.center for octant in bbox.octree(): if octant.point_inside(center): decomposition[octant].append(face) break decomposed = {octant: faces for octant, faces in decomposition.items() if faces} return decomposed
[docs] def octree_face_decomposition(face): """ Decomposes the face discretization triangle faces inside eight boxes from a bounding box octree structure. :param face: given face. :return: returns a dictionary containing bounding boxes as keys and as values, a list of faces inside that bounding box. """ triangulation = face.triangulation() triangulation_faces = triangulation.faces return octree_decomposition(face.bounding_box, triangulation_faces)
[docs] def parametric_face_inside(face1, face2, abs_tol: float = 1e-6): """ Verifies if a face2 is inside face1. It returns True if face2 is inside or False if the opposite. """ if face1.surface3d.is_coincident(face2.surface3d, abs_tol): if not face1.bounding_box.is_intersecting(face2.bounding_box) and \ not face1.bounding_box.is_inside_bbox(face2.bounding_box): return False self_contour2d = face1.surface2d.outer_contour face2_contour2d = face2.surface2d.outer_contour if self_contour2d.is_inside(face2_contour2d): if self_contour2d.is_inside(face2_contour2d): for inner_contour2d in face1.surface2d.inner_contours: if inner_contour2d.is_inside(face2_contour2d) or inner_contour2d.is_superposing( face2_contour2d ): return False return True if self_contour2d.is_superposing(face2_contour2d): return True return False
[docs] class Face3D(design3d.core.Primitive3D): """ Abstract method to define 3D faces. """ min_x_density = 1 min_y_density = 1 face_tolerance = 1e-6 def __init__(self, surface3d, surface2d: surfaces.Surface2D, reference_path: str = design3d.PATH_ROOT, name: str = ""): self.surface3d = surface3d self.surface2d = surface2d self._outer_contour3d = None self._inner_contours3d = None self._face_octree_decomposition = None self._primitives_mapping = None design3d.core.Primitive3D.__init__(self, reference_path=reference_path, name=name) # def to_dict(self, *args, **kwargs): # """Avoids storing points in memo that makes serialization slow.""" # return DessiaObject.to_dict(self, use_pointers=False) def __hash__(self): """Computes the hash.""" return hash(self.surface3d) + hash(self.surface2d) def __eq__(self, other_): """Computes the equality to another face.""" if other_.__class__.__name__ != self.__class__.__name__: return False equal = self.surface3d == other_.surface3d and self.surface2d == other_.surface2d return equal
[docs] def point_belongs(self, point3d: design3d.Point3D, tol: float = 1e-6): """ Tells you if a point is on the 3D face and inside its contour. """ if not self.bounding_box.point_inside(point3d, 1e-3): return False point2d = self.surface3d.point3d_to_2d(point3d) if not self.surface3d.point_belongs(point3d, tol): return False return self.surface2d.point_belongs(point2d)
@property def outer_contour3d(self) -> design3d.wires.Contour3D: """ Gives the 3d version of the outer contour of the face. """ if not self._outer_contour3d: outer_contour3d, primitives_mapping = self.surface3d.contour2d_to_3d(self.surface2d.outer_contour, return_primitives_mapping=True) self._outer_contour3d = outer_contour3d if self._primitives_mapping is None: self._primitives_mapping = primitives_mapping else: self._primitives_mapping.update(primitives_mapping) return self._outer_contour3d @outer_contour3d.setter def outer_contour3d(self, value): self._outer_contour3d = value @property def inner_contours3d(self) -> List[design3d.wires.Contour3D]: """ Gives the 3d version of the inner contours of the face. """ if not self._inner_contours3d: primitives_mapping = {} inner_contours3d = [] for contour2d in self.surface2d.inner_contours: inner_contour3d, contour2d_mapping = self.surface3d.contour2d_to_3d(contour2d, return_primitives_mapping=True) inner_contours3d.append(inner_contour3d) primitives_mapping.update(contour2d_mapping) self._inner_contours3d = inner_contours3d if self._primitives_mapping is None: self._primitives_mapping = primitives_mapping else: self._primitives_mapping.update(primitives_mapping) return self._inner_contours3d @inner_contours3d.setter def inner_contours3d(self, value): self._inner_contours3d = value @property def primitives_mapping(self): """ Gives the 3d version of the inner contours of the face. """ if not self._primitives_mapping or not self._inner_contours3d: if not self._primitives_mapping and self._outer_contour3d: self._outer_contour3d = None if not self._primitives_mapping and self._inner_contours3d: self._inner_contours3d = None _ = self.outer_contour3d _ = self.inner_contours3d return self._primitives_mapping @primitives_mapping.setter def primitives_mapping(self, primitives_mapping): self._primitives_mapping = primitives_mapping @property def bounding_box(self): """ Returns the surface bounding box. """ if not self._bbox: self._bbox = self.get_bounding_box() return self._bbox @bounding_box.setter def bounding_box(self, new_bouding_box): """Get the bounding box of the face.""" self._bbox = new_bouding_box
[docs] def get_bounding_box(self): """General method to get the bounding box of a face 3D.""" return self.outer_contour3d.bounding_box
[docs] def area(self): """Computes the area of the surface 2d.""" return self.surface2d.area()
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a Face3D. :param arguments: The arguments of the step primitive. :type arguments: list :param object_dict: The dictionary containing all the step primitives that have already been instantiated. :type object_dict: dict :return: The corresponding Face3D object. :rtype: :class:`design3d.faces.Face3D` """ step_id = kwargs.get("step_id", "#UNKNOW_ID") step_name = kwargs.get("name", "ADVANCED_FACE") name = arguments[0][1:-1] if arguments[1] != "()": contours = [object_dict[int(arg[1:])] for arg in arguments[1]] else: contours = [] if any(contour is None for contour in contours): warnings.warn( f"Could not instantiate #{step_id} = {step_name}({arguments})" f" because some of the contours are NoneType." "See Face3D.from_step method" ) return None surface = object_dict[int(arguments[2])] face = globals()[surface.face_class] point_in_contours3d = any(isinstance(contour, design3d.Point3D) for contour in contours) if not contours or ((len(contours) == 1) and isinstance(contours[0], design3d.Point3D)): return face.from_surface_rectangular_cut(surface) if len(contours) == 2 and point_in_contours3d: vertex = next(contour for contour in contours if isinstance(contour, design3d.Point3D)) base = next(contour for contour in contours if contour is not vertex) return face.from_base_and_vertex(surface, base, vertex, name) if point_in_contours3d: point = next(contour for contour in contours if isinstance(contour, design3d.Point3D)) contours = [contour for contour in contours if contour is not point] return face.from_contours3d_and_rectangular_cut(surface, contours, point) return face.from_contours3d(surface, contours, name)
[docs] @classmethod def from_contours3d(cls, surface, contours3d: List[design3d.wires.Contour3D], name: str = ""): """ Returns the face generated by a list of contours. Finds out which are outer or inner contours. :param surface: Surface3D where the face is defined. :param contours3d: List of 3D contours representing the face's BREP. :param name: the name to inject in the new face """ outer_contour3d, inner_contours3d = None, [] if len(contours3d) == 1: outer_contour2d, primitives_mapping = surface.contour3d_to_2d(contours3d[0], return_primitives_mapping=True) outer_contour3d = contours3d[0] inner_contours2d = [] elif len(contours3d) > 1: outer_contour2d, inner_contours2d, outer_contour3d, \ inner_contours3d, primitives_mapping = cls.from_contours3d_with_inner_contours(surface, contours3d) else: raise ValueError('Must have at least one contour') if ((not outer_contour2d) or (not all(outer_contour2d.primitives)) or (not surface.brep_connectivity_check(outer_contour2d, tol=5e-5))): return None # if outer_contour3d and outer_contour3d.primitives and not outer_contour3d.is_ordered(1e-5): # outer_contour2d = contour2d_healing(outer_contour2d) face = cls( surface, surface2d=surfaces.Surface2D(outer_contour=outer_contour2d, inner_contours=inner_contours2d), name=name, ) # To improve performance while reading from step file face.outer_contour3d = outer_contour3d face.inner_contours3d = inner_contours3d face.primitives_mapping = primitives_mapping return face
[docs] @staticmethod def from_contours3d_with_inner_contours(surface, contours3d): """Helper function to class.""" outer_contour2d = None outer_contour3d = None inner_contours2d = [] inner_contours3d = [] primitives_mapping = {} contours2d = [] for contour3d in contours3d: contour2d, contour_mapping = surface.contour3d_to_2d(contour3d, return_primitives_mapping=True) contours2d.append(contour2d) primitives_mapping.update(contour_mapping) check_contours = [not contour2d.is_ordered(tol=1e-2) for contour2d in contours2d] if (surface.x_periodicity or surface.y_periodicity) and sum(1 for value in check_contours if value) >= 2: outer_contour2d, inner_contours2d = surface.connect_contours(contours2d[0], contours2d[1:]) outer_contour3d, primitives_mapping = surface.contour2d_to_3d(outer_contour2d, return_primitives_mapping=True) inner_contours3d = [] for contour2d in inner_contours2d: contour3d, contour_mapping = surface.contour2d_to_3d(contour2d, return_primitives_mapping=True) inner_contours3d.append(contour3d) primitives_mapping.update(contour_mapping) else: if contours3d[0].name == "face_outer_bound": outer_contour2d, inner_contours2d = contours2d[0], contours2d[1:] outer_contour3d, inner_contours3d = contours3d[0], contours3d[1:] if surface.x_periodicity or surface.y_periodicity: Face3D.helper_repair_inner_contours_periodicity(surface, outer_contour2d, inner_contours2d, primitives_mapping) else: area = -1 for contour2d, contour3d in zip(contours2d, contours3d): inner_contours2d.append(contour2d) inner_contours3d.append(contour3d) contour_area = contour2d.area() if contour_area > area: area = contour_area outer_contour2d = contour2d outer_contour3d = contour3d inner_contours2d.remove(outer_contour2d) inner_contours3d.remove(outer_contour3d) if surface.x_periodicity or surface.y_periodicity: Face3D.helper_repair_inner_contours_periodicity(surface, outer_contour2d, inner_contours2d, primitives_mapping) return outer_contour2d, inner_contours2d, outer_contour3d, inner_contours3d, primitives_mapping
[docs] @staticmethod def helper_repair_inner_contours_periodicity(surface, outer_contour2d, inner_contours2d, primitives_mapping): """Translate inner contours if it's not inside the outer contour of the face.""" outer_contour2d_brec = outer_contour2d.bounding_rectangle umin_bound, umax_bound, d3din_bound, d3dax_bound = outer_contour2d_brec.bounds() def helper_translate_contour(inner_contour, inner_contour_brec): umin, umax, d3din, d3dax = inner_contour_brec.bounds() delta_x = 0.0 delta_y = 0.0 if surface.x_periodicity: if umin > umax_bound: delta_x = -surface.x_periodicity elif umax < umin_bound: delta_x = surface.x_periodicity if surface.y_periodicity: if d3din > d3dax_bound: delta_y = -surface.y_periodicity elif d3dax < d3din_bound: delta_y = surface.y_periodicity translation_vector = design3d.Vector2D(delta_x, delta_y) return inner_contour.translation(translation_vector) for i, _inner_contour in enumerate(inner_contours2d): if not outer_contour2d_brec.is_inside_b_rectangle(_brec := _inner_contour.bounding_rectangle): inner_contours2d[i] = helper_translate_contour(_inner_contour, _brec) for new_primitive, old_primitive in zip(inner_contours2d[i].primitives, _inner_contour.primitives): primitives_mapping[new_primitive] = primitives_mapping.pop(old_primitive)
[docs] def to_step(self, current_id): """Transforms a Face 3D into a Step object.""" content, surface3d_ids = self.surface3d.to_step(current_id) current_id = max(surface3d_ids) if len(surface3d_ids) != 1: raise NotImplementedError("What to do with more than 1 id ? with 0 id ?") outer_contour_content, outer_contour_id = self.outer_contour3d.to_step( current_id, surface_id=surface3d_ids[0], surface3d=self.surface3d ) content += outer_contour_content content += f"#{outer_contour_id + 1} = FACE_OUTER_BOUND('{self.name}',#{outer_contour_id},.T.);\n" contours_ids = [outer_contour_id + 1] current_id = outer_contour_id + 2 for inner_contour3d in self.inner_contours3d: inner_contour_content, inner_contour_id = inner_contour3d.to_step(current_id) # surface_id=surface3d_id) content += inner_contour_content face_bound_id = inner_contour_id + 1 content += f"#{face_bound_id} = FACE_BOUND('',#{inner_contour_id},.T.);\n" contours_ids.append(face_bound_id) current_id = face_bound_id + 1 content += ( f"#{current_id} = ADVANCED_FACE('{self.name}',({design3d.core.step_ids_to_str(contours_ids)})" f",#{surface3d_ids[0]},.T.);\n" ) # TODO: create an ADVANCED_FACE for each surface3d_ids ? return content, [current_id]
[docs] def triangulation_lines(self): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ return [], []
[docs] @staticmethod def get_edge_discretization_size(edge3d): """ Helper function to polygonize the face boundaries. """ angle_resolution = 10 if edge3d is None: number_points = 2 elif edge3d.__class__.__name__ == "BSplineCurve3D": number_points = max(15, len(edge3d.ctrlpts)) elif edge3d.__class__.__name__ in ("Arc3D", "FullArc3D", "ArcEllipse3D", "FullArcEllipse3D"): number_points = max(2, math.ceil(round(edge3d.angle, 12) / math.radians(angle_resolution)) + 1) else: number_points = 2 return number_points
[docs] def grid_points(self, grid_size, polygon_data=None): """ Parametric tesselation points. """ if polygon_data: outer_polygon, inner_polygons = polygon_data else: outer_polygon, inner_polygons = self.get_face_polygons() u, v = self._get_grid_axis(outer_polygon, grid_size) if not u or not v: return [] if inner_polygons: grid_points = [] points_indexes_map = {} for j, v_j in enumerate(v): for i, u_i in enumerate(u): grid_points.append((u_i, v_j)) points_indexes_map[(i, j)] = len(grid_points) - 1 grid_points = update_face_grid_points_with_inner_polygons(inner_polygons, [grid_points, u, v, points_indexes_map]) else: grid_points = np.array([[u_i, v_j] for v_j in v for u_i in u], dtype=np.float64) grid_points = self._update_grid_points_with_outer_polygon(outer_polygon, grid_points) return grid_points
@staticmethod def _get_grid_axis(outer_polygon, grid_size): """Helper function to grid_points.""" u_min, u_max, v_min, v_max = outer_polygon.bounding_rectangle.bounds() number_points_u, number_points_v = grid_size u_step = (u_max - u_min) / (number_points_u - 1) if number_points_u > 1 else (u_max - u_min) v_step = (v_max - v_min) / (number_points_v - 1) if number_points_v > 1 else (v_max - v_min) u = [u_min + i * u_step for i in range(number_points_u)] v = [v_min + i * v_step for i in range(number_points_v)] return u, v @staticmethod def _update_grid_points_with_outer_polygon(outer_polygon, grid_points): """Helper function to grid_points.""" # Find the indices where points_in_polygon is True (i.e., points inside the polygon) indices = np.where(outer_polygon.points_in_polygon(grid_points, include_edge_points=False) == 0)[0] grid_points = np.delete(grid_points, indices, axis=0) polygon_points = set(outer_polygon.points) points = [design3d.Point2D(*point) for point in grid_points if design3d.Point2D(*point) not in polygon_points] return points
[docs] def get_face_polygons(self): """Get face polygons.""" primitives_mapping = self.primitives_mapping def get_polygon_points(primitives): points = [] for edge in primitives: edge3d = primitives_mapping.get(edge) number_points = self.get_edge_discretization_size(edge3d) edge_points = edge.discretization_points(number_points=number_points) points.extend(edge_points[:-1]) return points outer_polygon = design3d.wires.ClosedPolygon2D(get_polygon_points(self.surface2d.outer_contour.primitives)) inner_polygons = [design3d.wires.ClosedPolygon2D(get_polygon_points(inner_contour.primitives)) for inner_contour in self.surface2d.inner_contours] return outer_polygon, inner_polygons
[docs] def grid_size(self): """ Specifies an adapted size of the discretization grid used in face triangulation. """ return 0, 0
[docs] @staticmethod def helper_triangulation_without_holes(vertices, segments, points_grid, tri_opt): """ Triangulates a surface without holes. :param vertices: vertices of the surface. :param segments: segments defined as tuples of vertices. :param points_grid: to do. :param tri_opt: triangulation option: "p" :return: """ vertices_grid = [(p.x, p.y) for p in points_grid] vertices.extend(vertices_grid) tri = {'vertices': np.array(vertices).reshape((-1, 2)), 'segments': np.array(segments).reshape((-1, 2)), } triangulation = triangle_lib.triangulate(tri, tri_opt) return d3dd.Mesh2D(triangulation['vertices'], triangles=triangulation['triangles'])
[docs] def helper_to_mesh(self, polygon_data=None) -> design3d.display.Mesh2D: """ Triangulates the Surface2D using the Triangle library. :param polygon_data: Face's outer polygon. :type polygon_data: Union[Tuple((wires.ClosedPolygon2D), List[wires.ClosedPolygon2D], None] :return: The triangulated surface as a display mesh. :rtype: :class:`design3d.display.Mesh2D` """ area = self.surface2d.bounding_rectangle().area() tri_opt = "p" if math.isclose(area, 0., abs_tol=1e-8): return None grid_size = self.grid_size() points_grid = [] if polygon_data: outer_polygon, inner_polygons = polygon_data else: outer_polygon, inner_polygons = self.get_face_polygons() if any(grid_size): points_grid = self.grid_points(grid_size, [outer_polygon, inner_polygons]) points = outer_polygon.points.copy() if len(set(points)) < len(points): return None vertices = [(point.x, point.y) for point in points] n = len(points) segments = [(i, i + 1) for i in range(n - 1)] segments.append((n - 1, 0)) if not inner_polygons: # No holes return self.helper_triangulation_without_holes(vertices, segments, points_grid, tri_opt) point_index = {p: i for i, p in enumerate(points)} holes = [] for inner_polygon in inner_polygons: inner_polygon_nodes = inner_polygon.points for point in inner_polygon_nodes: if point not in point_index: points.append(point) vertices.append((point.x, point.y)) point_index[point] = n n += 1 for point1, point2 in zip(inner_polygon_nodes[:-1], inner_polygon_nodes[1:]): segments.append((point_index[point1], point_index[point2])) segments.append((point_index[inner_polygon_nodes[-1]], point_index[inner_polygon_nodes[0]])) rpi = inner_polygon.barycenter() if not inner_polygon.point_inside(rpi, include_edge_points=False): rpi = inner_polygon.random_point_inside(include_edge_points=False) holes.append([rpi.x, rpi.y]) if points_grid: vertices_grid = [(p.x, p.y) for p in points_grid] vertices.extend(vertices_grid) tri = {'vertices': np.array(vertices).reshape((-1, 2)), 'segments': np.array(segments).reshape((-1, 2)), 'holes': np.array(holes).reshape((-1, 2)) } triangulation = triangle_lib.triangulate(tri, tri_opt) return d3dd.Mesh2D(triangulation['vertices'], triangles=triangulation['triangles'].astype(np.int32))
[docs] def triangulation(self): """Triangulates the face.""" outer_polygon, inner_polygons = self.get_face_polygons() mesh2d = self.helper_to_mesh([outer_polygon, inner_polygons]) if mesh2d is None: return None return d3dd.Mesh3D(self.surface3d.parametric_points_to_3d(mesh2d.vertices), mesh2d.triangles)
[docs] def plot2d(self, ax=None, color="k", alpha=1): """Plot 2D of the face using matplotlib.""" if ax is None: _, ax = plt.subplots() self.surface2d.plot(ax=ax, color=color, alpha=alpha)
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ Face3D rotation. :param center: rotation center :param axis: rotation axis :param angle: angle rotation :return: a new rotated Face3D """ new_surface = self.surface3d.rotation(center=center, axis=axis, angle=angle) return self.__class__(new_surface, self.surface2d)
[docs] def translation(self, offset: design3d.Vector3D): """ Face3D translation. :param offset: Translation vector. :type offset: `design3d.Vector3D` :return: A new translated Face3D """ new_surface3d = self.surface3d.translation(offset=offset) return self.__class__(new_surface3d, self.surface2d)
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new Face3D. side = 'old' or 'new' """ new_surface3d = self.surface3d.frame_mapping(frame, side) return self.__class__(new_surface3d, self.surface2d.copy(), self.name)
[docs] def copy(self, deep=True, memo=None): """Returns a copy of the Face3D.""" return self.__class__(self.surface3d.copy(deep=deep, memo=memo), self.surface2d.copy(), self.name)
[docs] def face_inside(self, face2, abs_tol: float = 1e-6): """ Verifies if a face is inside another one. It returns True if face2 is inside or False if the opposite. """ if self.surface3d.is_coincident(face2.surface3d, abs_tol): if not self.bounding_box.is_intersecting(face2.bounding_box) and \ not self.bounding_box.is_inside_bbox(face2.bounding_box): return False self_contour2d = self.outer_contour3d.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) face2_contour2d = face2.outer_contour3d.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) if self_contour2d.is_inside(face2_contour2d): if self_contour2d.is_inside(face2_contour2d): for inner_contour in self.inner_contours3d: inner_contour2d = inner_contour.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) if inner_contour2d.is_inside(face2_contour2d) or inner_contour2d.is_superposing( face2_contour2d ): return False return True if self_contour2d.is_superposing(face2_contour2d): return True return False
[docs] def edge_intersections(self, edge): """Gets the intersections of an edge and a 3D face.""" intersections = [] method_name = f"{edge.__class__.__name__.lower()[:-2]}_intersections" if hasattr(self, method_name): intersections = getattr(self, method_name)(edge) elif hasattr(self.surface3d, method_name): edge_surface_intersections = getattr(self.surface3d, method_name)(edge) for intersection in edge_surface_intersections: if self.point_belongs(intersection, self.face_tolerance) and not intersection.in_list(intersections): intersections.append(intersection) if not intersections: for point in [edge.start, edge.end]: if self.point_belongs(point): if point not in intersections: intersections.append(point) return intersections
[docs] def line_intersections(self, line: design3d_curves.Line3D) -> List[design3d.Point3D]: """ Get intersections between a face 3d and a Line 3D. :param line: other line. :return: a list of intersections. """ intersections = [] for intersection in self.surface3d.line_intersections(line): if self.point_belongs(intersection): intersections.append(intersection) if not intersections: for prim in self.outer_contour3d.primitives: intersection = prim.line_intersections(line) if intersection: if intersection not in intersections: intersections.append(intersection) return intersections
[docs] def linesegment_intersections(self, linesegment: d3de.LineSegment3D, abs_tol: float = 1e-6) -> List[design3d.Point3D]: """ Get intersections between a face 3d and a Line Segment 3D. :param linesegment: other linesegment. :param abs_tol: tolerance used. :return: a list of intersections. """ linesegment_intersections = [] if not self.bounding_box.is_intersecting(linesegment.bounding_box): return [] for intersection in self.surface3d.linesegment_intersections(linesegment): if self.point_belongs(intersection, abs_tol): linesegment_intersections.append(intersection) return linesegment_intersections
[docs] def fullarc_intersections(self, fullarc: d3de.FullArc3D) -> List[design3d.Point3D]: """ Get intersections between a face 3d and a Full Arc 3D. :param fullarc: other fullarc. :return: a list of intersections. """ intersections = [] for intersection in self.surface3d.fullarc_intersections(fullarc): if self.point_belongs(intersection): intersections.append(intersection) return intersections
[docs] def plot(self, ax=None, color="k", alpha=1, edge_details=False): """Plots the face.""" if not ax: _, ax = plt.subplots(subplot_kw={"projection": "3d"}) self.outer_contour3d.plot( ax=ax, edge_style=EdgeStyle(color=color, alpha=alpha, edge_ends=edge_details, edge_direction=edge_details) ) for contour3d in self.inner_contours3d: contour3d.plot( ax=ax, edge_style=EdgeStyle(color=color, alpha=alpha, edge_ends=edge_details, edge_direction=edge_details), ) return ax
[docs] def random_point_inside(self): """Gets a random point on the face.""" point_inside2d = self.surface2d.random_point_inside() return self.surface3d.point2d_to_3d(point_inside2d)
[docs] def is_adjacent(self, face2: "Face3D"): """ Verifies if two faces are adjacent or not. :param face2: other face. :return: True or False. """ contour1 = self.outer_contour3d.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) contour2 = face2.outer_contour3d.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) if contour1.is_sharing_primitives_with(contour2): return True return False
[docs] def geo_lines(self): # , mesh_size_list=None): """ Gets the lines that define a Face3D in a .geo file. """ i_index, p_index = None, None lines, line_surface, lines_tags = [], [], [] point_account, line_account, line_loop_account = 0, 0, 1 for c_index, contour in enumerate(list(chain(*[[self.outer_contour3d], self.inner_contours3d]))): if isinstance(contour, design3d_curves.Circle2D): # point=[contour.radius, contour.center.y, 0] # lines.append('Point('+str(point_account+1)+') = {'+str(point)[1:-1]+', '+str(mesh_size)+'};') # point = [*contour.center, 0] # lines.append('Point('+str(point_account+2)+') = {'+str(point)[1:-1]+', '+str(mesh_size)+'};') # point=[-contour.radius, contour.center.y, 0] # lines.append('Point('+str(point_account+3)+') = {'+str(point)[1:-1]+', '+str(mesh_size)+'};') # lines.append('Circle('+str(line_account+1)+') = {'+str(point_account+1)+','+str(point_account+2) \ # +','+str(point_account+3)+'};') # lines.append('Circle('+str(line_account+2)+') = {'+str(point_account+3)+','+str(point_account+2) \ # + ','+str(point_account+1)+'};') # lines_tags.extend([line_account+1, line_account+2]) # lines.append('Line Loop('+str(line_loop_account+1)+') = {'+str(lines_tags)[1:-1]+'};') # line_surface.append(line_loop_account+1) # lines_tags = [] # point_account, line_account, line_loop_account = point_account+3, line_account+2, line_loop_account+1 pass elif isinstance(contour, (design3d.wires.Contour3D, design3d.wires.ClosedPolygon3D)): if not isinstance(contour, design3d.wires.ClosedPolygon3D): contour = contour.to_polygon(1) for i_index, point in enumerate(contour.points): lines.append(point.get_geo_lines(tag=point_account + i_index + 1, point_mesh_size=None)) for p_index, primitive in enumerate(contour.primitives): if p_index != len(contour.primitives) - 1: lines.append( primitive.get_geo_lines( tag=line_account + p_index + 1, start_point_tag=point_account + p_index + 1, end_point_tag=point_account + p_index + 2, ) ) else: lines.append( primitive.get_geo_lines( tag=line_account + p_index + 1, start_point_tag=point_account + p_index + 1, end_point_tag=point_account + 1, ) ) lines_tags.append(line_account + p_index + 1) lines.append("Line Loop(" + str(c_index + 1) + ") = {" + str(lines_tags)[1:-1] + "};") line_surface.append(line_loop_account) point_account = point_account + i_index + 1 line_account, line_loop_account = line_account + p_index + 1, line_loop_account + 1 lines_tags = [] lines.append("Plane Surface(" + str(1) + ") = {" + str(line_surface)[1:-1] + "};") return lines
[docs] def to_geo(self, file_name: str): # , mesh_size_list=None): """ Gets the .geo file for the Face3D. """ lines = self.geo_lines() with open(file_name + ".geo", "w", encoding="utf-8") as file: for line in lines: file.write(line) file.write("\n") file.close()
[docs] def get_geo_lines(self, tag: int, line_loop_tag: List[int]): """ Gets the lines that define a PlaneFace3D in a .geo file. """ return "Plane Surface(" + str(tag) + ") = {" + str(line_loop_tag)[1:-1] + "};"
[docs] def edge3d_inside(self, edge3d, abs_tol: float = 1e-6): """ Returns True if edge 3d is coplanar to the face. """ method_name = f"{edge3d.__class__.__name__.lower()[:-2]}_inside" if hasattr(self, method_name): return getattr(self, method_name)(edge3d) points = edge3d.discretization_points(number_points=10) for point in points[1:-1]: if not self.point_belongs(point, abs_tol): return False return True
[docs] def is_intersecting(self, face2, list_coincident_faces=None, tol: float = 1e-6): """ Verifies if two face are intersecting. :param face2: face 2 :param list_coincident_faces: list of coincident faces, if existent :param tol: tolerance for calculations :return: True if faces intersect, False otherwise """ if list_coincident_faces is None: list_coincident_faces = [] if self.bounding_box.is_intersecting(face2.bounding_box, tol) and (self, face2) not in list_coincident_faces: edge_intersections = [] for prim1 in self.outer_contour3d.primitives + [ prim for inner_contour in self.inner_contours3d for prim in inner_contour.primitives ]: edge_intersections = face2.edge_intersections(prim1) if edge_intersections: return True if not edge_intersections: for prim2 in face2.outer_contour3d.primitives + [ prim for inner_contour in face2.inner_contours3d for prim in inner_contour.primitives ]: edge_intersections = self.edge_intersections(prim2) if edge_intersections: return True return False
[docs] def face_intersections_outer_contour(self, face2): """ Returns the intersections of the face outer contour with other given face. """ intersections_points = [] for edge1 in self.outer_contour3d.primitives: intersection_points = face2.edge_intersections(edge1) if intersection_points: for point in intersection_points: if not point.in_list(intersections_points): intersections_points.append(point) return intersections_points
[docs] def face_border_intersections(self, face2): """ Returns the intersections of the face outer and inner contour with other given face. """ intersections_points = [] for contour in [self.outer_contour3d] + self.inner_contours3d: for edge1 in contour.primitives: intersection_points = face2.edge_intersections(edge1) for point in intersection_points: if not point.in_list(intersections_points): intersections_points.append(point) return intersections_points
[docs] def face_intersections(self, face2, tol=1e-6) -> List[design3d.wires.Wire3D]: """ Calculates the intersections between two Face3D. """ bbox1 = self.bounding_box bbox2 = face2.bounding_box if not bbox1.is_intersecting(bbox2, tol): return [] if self.face_inside(face2) or face2.face_inside(self): return [] face_intersections = self.get_face_intersections(face2) return face_intersections
def _generic_face_intersections(self, generic_face): """ Calculates the intersections between two Faces 3D. :param generic_face: the other Face 3D to verify intersections with. :return: list of intersecting wires. """ surface_intersections = self.surface3d.surface_intersections(generic_face.surface3d) intersections_points = self.face_intersections_outer_contour(generic_face) for point in generic_face.face_intersections_outer_contour(self): if not point.in_list(intersections_points): intersections_points.append(point) face_intersections = [] for primitive in surface_intersections: points_on_primitive = [] for point in intersections_points: if primitive.point_belongs(point, 1e-4): points_on_primitive.append(point) if not points_on_primitive: continue points_on_primitive = primitive.sort_points_along_curve(points_on_primitive) if primitive.periodic: points_on_primitive = points_on_primitive + [points_on_primitive[0]] for point1, point2 in zip(points_on_primitive[:-1], points_on_primitive[1:]): edge = primitive.trim(point1, point2) if self.edge3d_inside(edge, 1e-3) and generic_face.edge3d_inside(edge, 1e-3): face_intersections.append(design3d.wires.Wire3D([edge])) return face_intersections
[docs] def get_face_intersections(self, face2): """ Gets the intersections between two faces. :param face2: second face. :return: intersections. """ method_name = f"{face2.__class__.__name__.lower()[:-2]}_intersections" if hasattr(self, method_name): return getattr(self, method_name)(face2) return self._generic_face_intersections(face2)
[docs] def set_operations_new_faces(self, intersecting_combinations): """ Gets boolean operations new faces after splitting. :param intersecting_combinations: faces intersecting combinations dictionary. :return: new split faces. """ self_copy = self.copy(True) list_cutting_contours = self_copy.get_face_cutting_contours(intersecting_combinations) if not list_cutting_contours: return [self_copy] return self_copy.divide_face(list_cutting_contours)
[docs] def split_inner_contour_intersecting_cutting_contours(self, list_cutting_contours): """ Given a list contours cutting the face, it calculates inner contours intersections with these contours. Then, these inner contours were split at the found intersecting points. :param list_cutting_contours: list of contours cutting face. :return: """ list_split_inner_contours = [] for inner_contour in self.surface2d.inner_contours: list_intersecting_points_with_inner_contour = [] for cutting_contour in list_cutting_contours: contour_intersection_points = inner_contour.intersection_points(cutting_contour) if not contour_intersection_points: continue list_intersecting_points_with_inner_contour.extend(contour_intersection_points) inner_contour_intersections_with_outer_contour = inner_contour.intersection_points( self.surface2d.outer_contour ) if list_intersecting_points_with_inner_contour and inner_contour_intersections_with_outer_contour: list_intersecting_points_with_inner_contour.extend(inner_contour_intersections_with_outer_contour) sorted_intersections_points_along_inner_contour = inner_contour.sort_points_along_wire( list_intersecting_points_with_inner_contour ) if sorted_intersections_points_along_inner_contour: list_split_inner_contours.extend( inner_contour.split_with_sorted_points(sorted_intersections_points_along_inner_contour) ) # remove split_inner_contour connected to a cutting_contour at two points. connected_at_two_ends = [] for cutting_contour in list_cutting_contours: for split_contour in list_split_inner_contours: if split_contour.point_belongs( cutting_contour.primitives[0].start ) and split_contour.point_belongs(cutting_contour.primitives[-1].end): connected_at_two_ends.append(split_contour) break list_split_inner_contours = [ split_contour for split_contour in list_split_inner_contours if split_contour not in connected_at_two_ends ] return list_split_inner_contours
def _helper_validate_cutting_contours(self, list_cutting_contours, list_split_inner_contours): """ Helper method to validate list of cutting contours. :param list_cutting_contours: list of cutting contours. :param list_split_inner_contours: list of split inner contours. :return: valid list of cutting contours. """ valid_cutting_contours = [] while list_cutting_contours: for i, cutting_contour in enumerate(list_cutting_contours[:]): if ( self.surface2d.outer_contour.point_belongs(cutting_contour.primitives[0].start) and self.surface2d.outer_contour.point_belongs(cutting_contour.primitives[-1].end) ) or cutting_contour.primitives[0].start.is_close(cutting_contour.primitives[-1].end): valid_cutting_contours.append(cutting_contour) list_cutting_contours.pop(i) break list_cutting_contours.pop(i) while True: connecting_split_contour = cutting_contour.get_connected_wire(list_split_inner_contours) if not connecting_split_contour: valid_cutting_contours.append(cutting_contour) break list_split_inner_contours.remove(connecting_split_contour) new_contour = design3d.wires.Contour2D.contours_from_edges( cutting_contour.primitives + connecting_split_contour.primitives )[0] if ( self.surface2d.outer_contour.are_extremity_points_touching(new_contour) or new_contour.is_contour_closed() ): valid_cutting_contours.append(new_contour) break connecting_cutting_contour = new_contour.get_connected_wire(list_cutting_contours) if not connecting_cutting_contour: if any( self.surface2d.outer_contour.point_belongs(point) for point in [new_contour.primitives[0].start, new_contour.primitives[-1].end] ) and any( valid_contour.point_belongs(point) for valid_contour in valid_cutting_contours for point in [new_contour.primitives[0].start, new_contour.primitives[-1].end] ): valid_cutting_contours.append(new_contour) break new_contour = design3d.wires.Contour2D.contours_from_edges( new_contour.primitives + connecting_cutting_contour.primitives )[0] list_cutting_contours.remove(connecting_cutting_contour) if self.surface2d.outer_contour.are_extremity_points_touching(new_contour): valid_cutting_contours.append(new_contour) break cutting_contour = new_contour break return valid_cutting_contours
[docs] def get_face_cutting_contours(self, dict_intersecting_combinations): """ Get all contours cutting the face, resulting from multiple faces intersections. :param dict_intersecting_combinations: dictionary containing as keys the combination of intersecting faces and as the values the resulting primitive from the intersection of these two faces return a list all contours cutting one particular face. """ face_intersecting_primitives2d = self.select_face_intersecting_primitives(dict_intersecting_combinations) if not face_intersecting_primitives2d: return [] list_cutting_contours = design3d.wires.Contour2D.contours_from_edges(face_intersecting_primitives2d[:]) cutting_contours = [] if len(list_cutting_contours) > 1: while list_cutting_contours: closed_contours = [] opened_contours = [] for contour in list_cutting_contours: if contour.is_contour_closed(): closed_contours.append(contour) else: opened_contours.append(contour) list_cutting_contours = closed_contours + opened_contours current_cutting_contour = list_cutting_contours.pop(0) connected_contour = current_cutting_contour.get_connected_wire(list_cutting_contours) if connected_contour in list_cutting_contours: list_cutting_contours.remove(connected_contour) if not connected_contour: cutting_contours.append(current_cutting_contour) continue new_contour = current_cutting_contour.merge_not_adjacent_contour(connected_contour) list_cutting_contours.append(new_contour) list_cutting_contours = cutting_contours list_split_inner_contours = self.split_inner_contour_intersecting_cutting_contours(list_cutting_contours) valid_cutting_contours = self._helper_validate_cutting_contours( list_cutting_contours, list_split_inner_contours ) return valid_cutting_contours + list_split_inner_contours
[docs] def divide_face(self, list_cutting_contours: List[design3d.wires.Contour2D], abs_tol: float = 1e-6): """ Divides a Face 3D with a list of cutting contours. :param list_cutting_contours: list of contours cutting the face. :param abs_tol: tolerance. """ list_faces = [] list_open_cutting_contours = [] list_closed_cutting_contours = [] face_inner_contours = self.surface2d.inner_contours[:] list_cutting_contours_ = [] while list_cutting_contours: cutting_contour = list_cutting_contours[0] if not cutting_contour.primitives[0].start.is_close(cutting_contour.primitives[-1].end): list_cutting_contours_.append(cutting_contour) list_cutting_contours.remove(cutting_contour) continue for inner_contour in face_inner_contours: if cutting_contour.is_inside(inner_contour): if cutting_contour.is_sharing_primitives_with(inner_contour): merged_contours = cutting_contour.merge_with(inner_contour) list_cutting_contours.remove(cutting_contour) cutting_contour = merged_contours[0] # list_cutting_contours = merged_contours + list_cutting_contours # break continue if cutting_contour.is_sharing_primitives_with(inner_contour): merged_contours = cutting_contour.merge_with(inner_contour) list_cutting_contours.remove(cutting_contour) face_inner_contours.remove(inner_contour) list_cutting_contours = merged_contours + list_cutting_contours break else: list_cutting_contours_.append(cutting_contour) if cutting_contour in list_cutting_contours: list_cutting_contours.remove(cutting_contour) list_cutting_contours = list_cutting_contours_ list_cutting_contours_ = [] for cutting_contour in list_cutting_contours: contour_intersections = self.surface2d.outer_contour.wire_intersections(cutting_contour) if len(contour_intersections) >= 2: contour_intersections = sorted(contour_intersections, key=cutting_contour.abscissa) list_cutting_contours_.extend(cutting_contour.split_with_sorted_points(contour_intersections)) continue list_cutting_contours_.append(cutting_contour) list_cutting_contours = list_cutting_contours_ for cutting_contour in list_cutting_contours: if not cutting_contour.primitives[0].start.is_close(cutting_contour.primitives[-1].end): list_open_cutting_contours.append(cutting_contour) continue list_closed_cutting_contours.append(cutting_contour) if list_open_cutting_contours: list_faces = self.divide_face_with_open_cutting_contours(list_open_cutting_contours, abs_tol) list_faces = self.divide_face_with_closed_cutting_contours(list_closed_cutting_contours, list_faces) list_faces = [face for face in list_faces if not math.isclose(face.area(), 0.0, abs_tol=1e-08)] return list_faces
[docs] def divide_face_with_open_cutting_contours(self, list_open_cutting_contours, abs_tol: float = 1e-6): """ Divides a face 3D with a list of closed cutting contour, that is, it will cut holes on the face. :param list_open_cutting_contours: list containing the open cutting contours. :param abs_tol: tolerance. :return: list divided faces. """ list_faces = [] if not self.surface2d.outer_contour.edge_polygon.is_trigo: self.surface2d.outer_contour = self.surface2d.outer_contour.invert() new_faces_contours = self.surface2d.outer_contour.divide(list_open_cutting_contours, self.face_tolerance) new_inner_contours = len(new_faces_contours) * [[]] if self.surface2d.inner_contours: new_faces_contours, new_inner_contours = self.get_open_contour_divided_faces_inner_contours( new_faces_contours, abs_tol) if isinstance(self, Triangle3D): class_to_instanciate = PlaneFace3D else: class_to_instanciate = self.__class__ for contour, inner_contours in zip(new_faces_contours, new_inner_contours): new_face = class_to_instanciate(self.surface3d, surfaces.Surface2D(contour, inner_contours)) list_faces.append(new_face) return list_faces
[docs] def divide_face_with_closed_cutting_contours(self, list_closed_cutting_contours, list_faces): """ Divides a Face3D with a list of Open cutting contours. Contours going from one side to another of the Face, or from the outer contour to one inner contour. :param list_closed_cutting_contours: list containing the closed cutting contours :param list_faces: list of already divided faces :return: list divided faces """ for closed_cutting_contour in list_closed_cutting_contours: if closed_cutting_contour.area() / self.surface2d.outer_contour.area() > 1e-9 and\ closed_cutting_contour.primitives[0].start.is_close(closed_cutting_contour.primitives[-1].end): inner_contours1 = [] inner_contours2 = [] if list_faces: new_list_faces = self.get_closed_contour_divided_faces_inner_contours( list_faces, closed_cutting_contour ) list_faces = list_faces + new_list_faces continue new_contour_adjacent_to_inner_contour = False for inner_contour in self.surface2d.inner_contours: if closed_cutting_contour.is_inside(inner_contour): inner_contours2.append(inner_contour) continue if closed_cutting_contour.is_sharing_primitives_with(inner_contour): new_contour_adjacent_to_inner_contour = True inner_contours1.extend(closed_cutting_contour.merge_with(inner_contour)) else: inner_contours1.append(inner_contour) if not new_contour_adjacent_to_inner_contour: inner_contours1.append(closed_cutting_contour) if isinstance(self, Triangle3D): class_to_instanciate = PlaneFace3D else: class_to_instanciate = self.__class__ surf3d = self.surface3d surf2d = surfaces.Surface2D(self.surface2d.outer_contour, inner_contours1) new_plane = class_to_instanciate(surf3d, surf2d) list_faces.append(new_plane) list_faces.append( class_to_instanciate(surf3d, surfaces.Surface2D(closed_cutting_contour, inner_contours2)) ) continue surf3d = self.surface3d surf2d = surfaces.Surface2D(self.surface2d.outer_contour, []) if isinstance(self, Triangle3D): class_to_instanciate = PlaneFace3D else: class_to_instanciate = self.__class__ new_plane = class_to_instanciate(surf3d, surf2d) list_faces.append(new_plane) return list_faces
[docs] def get_open_contour_divided_faces_inner_contours(self, new_faces_contours, abs_tol: float = 1e-6): """ If there is any inner contour, verifies which ones belong to the new divided faces. :param new_faces_contours: new faces outer contour. :param abs_tol: tolerance. :return: valid_new_faces_contours, valid_new_faces_contours. """ valid_new_faces_contours = [] valid_inner_contours = [] new_faces_contours_ = [] for new_contour in new_faces_contours: for inner_contour in self.surface2d.inner_contours: if new_contour.is_superposing(inner_contour, abs_tol): break else: new_faces_contours_.append(new_contour) new_faces_contours = new_faces_contours_ while new_faces_contours: new_face_contour = new_faces_contours[0] if new_face_contour in valid_new_faces_contours: new_faces_contours.remove(new_face_contour) continue inner_contours = [] for inner_contour in self.surface2d.inner_contours: if not new_face_contour.is_inside(inner_contour): continue if new_face_contour.is_sharing_primitives_with(inner_contour): merged_new_face_contours = new_face_contour.merge_with(inner_contour) if merged_new_face_contours: new_faces_contours.remove(new_face_contour) new_faces_contours = merged_new_face_contours + new_faces_contours break else: inner_contours.append(inner_contour) else: valid_new_faces_contours.append(new_face_contour) valid_inner_contours.append(inner_contours) new_faces_contours.remove(new_face_contour) return valid_new_faces_contours, valid_inner_contours
[docs] def get_closed_contour_divided_faces_inner_contours(self, list_faces, new_contour): """ If there is any inner contour, verifies which ones belong to the new divided faces. :param list_faces: list of new faces. :param new_contour: current new face outer contour. :return: a list of new faces with its inner contours. """ new_list_faces = [] for new_face in list_faces: if new_face.surface2d.outer_contour.is_inside(new_contour): inner_contours1 = [] inner_contours2 = [] if not new_face.surface2d.inner_contours: new_face.surface2d.inner_contours = [new_contour] break new_contour_not_sharing_primitives = True for i, inner_contour in enumerate(new_face.surface2d.inner_contours): if new_contour.is_inside(inner_contour): if any(inner_contour.primitive_over_contour(prim) for prim in new_contour.primitives): new_face.surface2d.inner_contours[i] = new_contour break inner_contours2.append(inner_contour) elif not any(inner_contour.primitive_over_contour(prim) for prim in new_contour.primitives): inner_contours1.append(inner_contour) else: new_contour_not_sharing_primitives = False else: surf3d = new_face.surface3d if inner_contours1: if new_contour_not_sharing_primitives: inner_contours1.append(new_contour) new_face.surface2d.inner_contours = inner_contours1 new_list_faces.append(self.__class__(surf3d, surfaces.Surface2D(new_contour, []))) break surf2d = surfaces.Surface2D(new_face.surface2d.outer_contour, inner_contours1) new_plane = self.__class__(surf3d, surf2d) new_list_faces.append(new_plane) if inner_contours2: new_list_faces.append(self.__class__(surf3d, surfaces.Surface2D(new_contour, inner_contours2))) return new_list_faces
[docs] def select_face_intersecting_primitives(self, dict_intersecting_combinations): """ Select face intersecting primitives from a dictionary containing all intersection combinations. :param dict_intersecting_combinations: dictionary containing all intersection combinations :return: list of intersecting primitives for current face """ face_intersecting_primitives2d = [] intersections = dict_intersecting_combinations[self] for intersection_wire in intersections: wire2d = self.surface3d.contour3d_to_2d(intersection_wire) for primitive2d in wire2d.primitives: if self.surface3d.x_periodicity is not None and not \ self.surface2d.outer_contour.is_edge_inside(primitive2d): primitive_plus_periodicity = primitive2d.translation( design3d.Vector2D(self.surface3d.x_periodicity, 0)) if self.surface2d.outer_contour.is_edge_inside(primitive_plus_periodicity, self.face_tolerance): face_intersecting_primitives2d.append(primitive_plus_periodicity) continue primitive_minus_periodicity = primitive2d.translation( design3d.Vector2D(- self.surface3d.x_periodicity, 0)) if self.surface2d.outer_contour.is_edge_inside(primitive_minus_periodicity, self.face_tolerance): face_intersecting_primitives2d.append(primitive_minus_periodicity) continue if design3d.core.edge_in_list(primitive2d, face_intersecting_primitives2d) or design3d.core.edge_in_list( primitive2d.reverse(), face_intersecting_primitives2d ): continue if not self.surface2d.outer_contour.primitive_over_contour(primitive2d, tol=1e-7) and \ not any(inner_contour.primitive_over_contour(primitive2d, tol=1e-7) for inner_contour in self.surface2d.inner_contours): face_intersecting_primitives2d.append(primitive2d) return face_intersecting_primitives2d
def _is_linesegment_intersection_possible(self, linesegment: d3de.LineSegment3D): """ Verifies if intersection of face with line segment is possible or not. :param linesegment: other line segment. :return: returns True if possible, False otherwise. """ if not self.bounding_box.is_intersecting(linesegment.bounding_box): return False if math.isclose(self.area(), 0.0, abs_tol=1e-10): return False bbox_block_faces = design3d.primitives3d.Block.from_bounding_box(self.bounding_box).faces if not any(bbox_face.line_intersections(linesegment.line) for bbox_face in bbox_block_faces): return False return True def _get_linesegment_intersections_approximation(self, linesegment: d3de.LineSegment3D): """Generator line segment intersections approximation.""" if self.__class__ == PlaneFace3D: faces_triangulation = [self] else: triangulation = self.triangulation() faces_triangulation = triangulation.triangular_faces() for face in faces_triangulation: inters = face.linesegment_intersections(linesegment) yield inters
[docs] def is_linesegment_crossing(self, linesegment): """ Verify if a face 3d is being crossed by a line segment 3d. """ intersections = self.linesegment_intersections(linesegment) if intersections: return True return False
[docs] def linesegment_intersections_approximation(self, linesegment: d3de.LineSegment3D, abs_tol: float = 1e-6) -> List[design3d.Point3D]: """Approximation of intersections face 3D and a line segment 3D.""" if not self._is_linesegment_intersection_possible(linesegment): return [] linesegment_intersections = [] for inters in self._get_linesegment_intersections_approximation(linesegment): for point in inters: if not point.in_list(linesegment_intersections, abs_tol): linesegment_intersections.append(point) return linesegment_intersections
[docs] def face_decomposition(self): """ Decomposes the face discretization triangle faces inside eight boxes from a bounding box octree structure. """ if not self._face_octree_decomposition: self._face_octree_decomposition = octree_face_decomposition(self) return self._face_octree_decomposition
[docs] def face_minimum_distance(self, other_face, return_points: bool = False): """ Gets the minimum distance between two faces. :param other_face: second face to search for minimum distance. :param return_points: return corresponding point or not. :return: """ # Speficic case, if defined method_name = f"{other_face.__class__.__name__.lower()[:-2]}_minimum_distance" if hasattr(self, method_name): return getattr(self, method_name)(other_face, return_points) # Generic case return self.triangulation().minimum_distance(other_face.triangulation(), return_points)
# TODO : implement an exact method and then clean code # face_decomposition1 = self.face_decomposition() # face_decomposition2 = other_face.face_decomposition() # list_set_points1 = [ # {point for face in faces1 for point in face.points} for _, faces1 in face_decomposition1.items() # ] # list_set_points1 = [ # np.array([(point[0], point[1], point[2]) for point in sets_points1]) for sets_points1 in list_set_points1 # ] # list_set_points2 = [ # {point for face in faces2 for point in face.points} for _, faces2 in face_decomposition2.items() # ] # list_set_points2 = [ # np.array([(point[0], point[1], point[2]) for point in sets_points2]) for sets_points2 in list_set_points2 # ] # # minimum_distance = math.inf # index1, index2 = None, None # for sets_points1, sets_points2 in product(list_set_points1, list_set_points2): # print("zob") # distances = np.linalg.norm(sets_points2[:, np.newaxis] - sets_points1, axis=2) # sets_min_dist = np.min(distances) # if sets_min_dist < minimum_distance: # minimum_distance = sets_min_dist # index1 = next((i for i, x in enumerate(list_set_points1) if np.array_equal(x, sets_points1)), -1) # index2 = next((i for i, x in enumerate(list_set_points2) if np.array_equal(x, sets_points2)), -1) # # print(index1, index2) # faces1 = list(face_decomposition1.values())[index1] # faces2 = list(face_decomposition2.values())[index2] # # minimum_distance = math.inf # best_distance_points = None # # for face1, face2 in product(faces1, faces2): # distance, point1, point2 = face1.planeface_minimum_distance(face2, True) # if distance < minimum_distance: # minimum_distance = distance # best_distance_points = [point1, point2] # if return_points: # return minimum_distance, *best_distance_points # return minimum_distance
[docs] def plane_intersections(self, plane3d: surfaces.Plane3D): """ Gets intersections with a 3D plane surface. :param plane3d: The Plane3D instance to find intersections with. :type plane3d: Plane3D :return: List of Wire3D instances representing the intersections with the plane. :rtype: List[wires.Wire3D] """ surfaces_intersections = self.surface3d.plane_intersections(plane3d) outer_contour_intersections_with_plane = plane3d.contour_intersections(self.outer_contour3d) plane_intersections = [] for plane_intersection in surfaces_intersections: points_on_curve = [] for point in outer_contour_intersections_with_plane: if plane_intersection.point_belongs(point): points_on_curve.append(point) points_on_primitive = plane_intersection.sort_points_along_curve(points_on_curve) if not isinstance(plane_intersection, design3d_curves.Line3D): points_on_primitive = points_on_primitive + [points_on_primitive[0]] for point1, point2 in zip(points_on_primitive[:-1], points_on_primitive[1:]): edge = plane_intersection.trim(point1, point2) if self.edge3d_inside(edge): plane_intersections.append(design3d.wires.Wire3D([edge])) return plane_intersections
[docs] def split_by_plane(self, plane3d: surfaces.Plane3D): """Split face with a plane.""" intersections_with_plane = self.plane_intersections(plane3d) intersections_with_plane2d = [ self.surface3d.contour3d_to_2d(intersection_wire) for intersection_wire in intersections_with_plane ] while True: for i, intersection2d in enumerate(intersections_with_plane2d): if not self.surface2d.outer_contour.is_inside(intersection2d): for translation in [design3d.Vector2D(-2 * math.pi, 0), design3d.Vector2D(2 * math.pi, 0)]: translated_contour = intersection2d.translation(translation) if not self.surface2d.outer_contour.is_inside(translated_contour): continue intersections_with_plane2d.pop(i) intersections_with_plane2d.append(translated_contour) break else: break return self.divide_face(intersections_with_plane2d)
def _get_face_decomposition_set_closest_to_point(self, point): """ Searches for the faces decomposition's set closest to given point. :param point: other point. :return: list of triangular faces, corresponding to area of the face closest to point. """ face_decomposition1 = self.face_decomposition() list_set_points1 = [ {point for face in faces1 for point in face.points} for _, faces1 in face_decomposition1.items() ] list_set_points1 = [ np.array([(point[0], point[1], point[2]) for point in sets_points1]) for sets_points1 in list_set_points1 ] list_set_points2 = [np.array([(point[0], point[0], point[0])])] minimum_distance = math.inf index1 = None for sets_points1, sets_points2 in product(list_set_points1, list_set_points2): distances = np.linalg.norm(sets_points2[:, np.newaxis] - sets_points1, axis=2) sets_min_dist = np.min(distances) if sets_min_dist < minimum_distance: minimum_distance = sets_min_dist index1 = next((i for i, x in enumerate(list_set_points1) if np.array_equal(x, sets_points1)), -1) return list(face_decomposition1.values())[index1]
[docs] def point_distance(self, point, return_other_point: bool = False): """ Calculates the distance from a face 3d and a point. :param point: point to verify. :param return_other_point: bool to decide if corresponding point on face should be returned. :return: distance to face3D. """ faces1 = self._get_face_decomposition_set_closest_to_point(point) minimum_distance = math.inf best_distance_point = None for face1 in faces1: distance, point1 = face1.point_distance(point, True) if distance < minimum_distance: minimum_distance = distance best_distance_point = point1 if return_other_point: return minimum_distance, best_distance_point return minimum_distance
[docs] def get_coincident_face_intersections(self, face): """ Gets intersections for two faces which have coincident faces. :param face: other face. :return: two lists of intersections. one list containing wires intersecting face1, the other those for face2. """ points_intersections = self.outer_contour3d.wire_intersections(face.outer_contour3d) if not points_intersections: return [], [] extracted_contours = self.outer_contour3d.split_with_sorted_points(points_intersections) extracted_contours2 = face.outer_contour3d.split_with_sorted_points(points_intersections) contours_in_self = [ contour for contour in extracted_contours2 if all(self.edge3d_inside(edge) for edge in contour.primitives) ] contours_in_other_face = [ contour for contour in extracted_contours if all(face.edge3d_inside(edge) for edge in contour.primitives) ] return contours_in_self, contours_in_other_face
[docs] def normal_at_point(self, point): """ Gets Normal vector at a given point on the face. :param point: point on the face. :return: """ if not self.point_belongs(point): raise ValueError(f'Point {point} not in this face.') return self.surface3d.normal_at_point(point)
[docs] class PlaneFace3D(Face3D): """ Defines a PlaneFace3D class. :param surface3d: a plane 3d. :type surface3d: Plane3D. :param surface2d: a 2d surface to define the plane face. :type surface2d: Surface2D. """ def __init__(self, surface3d: surfaces.Plane3D, surface2d: surfaces.Surface2D, reference_path: str = design3d.PATH_ROOT, name: str = ""): self._bbox = None Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, reference_path=reference_path, name=name)
[docs] def copy(self, deep=True, memo=None): """Returns a copy of the PlaneFace3D.""" return PlaneFace3D(self.surface3d.copy(deep, memo), self.surface2d.copy(), self.reference_path, self.name)
[docs] def point_distance(self, point, return_other_point=False): """ Calculates the distance from a plane face and a point. :param point: point to verify. :param return_other_point: bool to decide if corresponding point on face should be returned. :return: distance to planeface3D. """ projected_pt = self.surface3d.point_projection(point) projection_distance = point.point_distance(projected_pt) if self.point_belongs(projected_pt): if return_other_point: return projection_distance, projected_pt return projection_distance point_2d = point.to_2d(self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v) polygon2d = self.surface2d.outer_contour.to_polygon(angle_resolution=10) border_distance, other_point = polygon2d.point_border_distance(point_2d, return_other_point=True) other_point = self.surface3d.point2d_to_3d(design3d.Point2D(*other_point)) if return_other_point: return (projection_distance ** 2 + border_distance ** 2) ** 0.5, other_point return (projection_distance ** 2 + border_distance ** 2) ** 0.5
[docs] def distance_to_point(self, point, return_other_point=False): """ Evaluates the distance to a given point. distance_to_point is deprecated, please use point_distance. """ warnings.warn("distance_to_point is deprecated, please use point_distance", category=DeprecationWarning) return self.point_distance(point, return_other_point)
[docs] def minimum_distance_points_plane(self, other_plane_face, return_points=False): """ Given two plane faces, calculates the points which corresponds to the minimal distance between these two faces. :param other_plane_face: Second plane face. :param return_points: Boolean to return corresponding points or not. :return: minimal distance. """ for edge in other_plane_face.outer_contour3d.primitives: edge_intersections = self.edge_intersections(edge) if edge_intersections: if return_points: return 0.0, edge_intersections[0], edge_intersections[0] return 0.0 min_distance = math.inf for edge1 in self.outer_contour3d.primitives: for edge2 in other_plane_face.outer_contour3d.primitives: if hasattr(edge1, "minimum_distance"): dist = edge1.minimum_distance(edge2, return_points=return_points) elif hasattr(edge2, "minimum_distance"): dist = edge2.minimum_distance(edge1, return_points=return_points) else: raise AttributeError(f"Neither {edge1} nor {edge2} has a minimum_distance method.") if return_points: if dist[0] < min_distance: min_distance = dist[0] point1, point2 = dist[1], dist[2] elif dist < min_distance: min_distance = dist if return_points: return min_distance, point1, point2 return min_distance
[docs] def linesegment_inside(self, linesegment: d3de.LineSegment3D): """ Verifies if a line segment 3D is completely inside the plane face. :param linesegment: the line segment to verify. :return: True if circle is inside False otherwise. """ if not linesegment.direction_vector().is_perpendicular_to(self.surface3d.frame.w, 1e-6): return False for point in [linesegment.start, linesegment.middle_point(), linesegment.end]: if not self.point_belongs(point): return False return True
[docs] def circle_inside(self, circle: design3d_curves.Circle3D): """ Verifies if a circle 3D is completely inside the plane face. :param circle: the circle to verify. :return: True if circle is inside False otherwise. """ if not math.isclose(abs(circle.frame.w.dot(self.surface3d.frame.w)), 1.0, abs_tol=1e-6): return False points = circle.discretization_points(number_points=4) for point in points: if not self.point_belongs(point): return False return True
[docs] def planeface_intersections(self, planeface): """ Calculates the intersections between two plane faces. :param planeface: the other Plane Face 3D to verify intersections with Plane Face 3D. :return: list of intersecting wires. """ face2_plane_intersections = planeface.surface3d.plane_intersections(self.surface3d) if not face2_plane_intersections: return [] points_intersections = self.face_border_intersections(planeface) for point in planeface.face_border_intersections(self): if not point.in_list(points_intersections): points_intersections.append(point) points_intersections = face2_plane_intersections[0].sort_points_along_curve(points_intersections) planeface_intersections = [] for point1, point2 in zip(points_intersections[:-1], points_intersections[1:]): linesegment3d = d3de.LineSegment3D(point1, point2) over_self_outer_contour = self.outer_contour3d.primitive_over_contour(linesegment3d) over_planeface_outer_contour = planeface.outer_contour3d.primitive_over_contour(linesegment3d) over_self_inner_contour = any( inner_contour.primitive_over_contour(linesegment3d) for inner_contour in self.inner_contours3d ) over_planeface_inner_contour = any( inner_contour.primitive_over_contour(linesegment3d) for inner_contour in planeface.inner_contours3d ) if over_self_inner_contour and over_planeface_outer_contour: continue if over_planeface_inner_contour and over_self_outer_contour: continue if over_self_outer_contour and over_planeface_outer_contour: continue if self.edge3d_inside(linesegment3d) or over_self_outer_contour: if planeface.edge3d_inside(linesegment3d) and not over_planeface_inner_contour: planeface_intersections.append(design3d.wires.Wire3D([linesegment3d])) elif over_planeface_outer_contour: planeface_intersections.append(design3d.wires.Wire3D([linesegment3d])) return planeface_intersections
[docs] def triangle_intersections(self, triangleface): """ Gets the intersections between a Plane Face3D and a Triangle3D. :param triangleface: the other triangle face. :return: """ return self.planeface_intersections(triangleface)
[docs] def cylindricalface_intersections(self, cylindricalface: "CylindricalFace3D"): """ Calculates the intersections between a plane face 3D and Cylindrical Face3D. :param cylindricalface: the Cylindrical Face 3D to verify intersections with Plane Face 3D. :return: list of intersecting wires. """ cylindricalsurfaceface_intersections = cylindricalface.surface3d.plane_intersections(self.surface3d) if not cylindricalsurfaceface_intersections: return [] if not isinstance(cylindricalsurfaceface_intersections[0], design3d_curves.Line3D): if all( self.edge3d_inside(intersection) and cylindricalface.edge3d_inside(intersection) for intersection in cylindricalsurfaceface_intersections ): if isinstance(cylindricalsurfaceface_intersections[0], design3d_curves.Circle3D): contour3d = design3d.wires.Contour3D( [design3d.edges.FullArc3D.from_curve(cylindricalsurfaceface_intersections[0])] ) else: contour3d = design3d.wires.Contour3D( [design3d.edges.FullArcEllipse3D.from_curve(cylindricalsurfaceface_intersections[0])] ) return [contour3d] intersections_points = self.face_intersections_outer_contour(cylindricalface) for point in cylindricalface.face_intersections_outer_contour(self): if point not in intersections_points: intersections_points.append(point) face_intersections = [] for primitive in cylindricalsurfaceface_intersections: points_on_primitive = [] for point in intersections_points: if primitive.point_belongs(point): points_on_primitive.append(point) if not points_on_primitive: continue points_on_primitive = primitive.sort_points_along_curve(points_on_primitive) if not isinstance(primitive, design3d_curves.Line3D): points_on_primitive = points_on_primitive + [points_on_primitive[0]] for point1, point2 in zip(points_on_primitive[:-1], points_on_primitive[1:]): edge = primitive.trim(point1, point2) if self.edge3d_inside(edge) and cylindricalface.edge3d_inside(edge): face_intersections.append(design3d.wires.Wire3D([edge])) return face_intersections
[docs] def conicalface_intersections(self, conical_face: "ConicalFace3D"): """ Calculates the intersections between a plane face 3D and Conical Face3D. :param conical_face: the Conical Face 3D to verify intersections with Plane Face 3D. :return: list of intersecting wires. """ surface_intersections = self.surface3d.surface_intersections(conical_face.surface3d) if isinstance(surface_intersections[0], design3d_curves.Circle3D): if self.edge3d_inside(surface_intersections[0]) and conical_face.edge3d_inside(surface_intersections[0]): contour3d = design3d.wires.Contour3D([design3d.edges.FullArc3D.from_curve(surface_intersections[0])]) return [contour3d] if isinstance(surface_intersections[0], design3d_curves.Ellipse3D): if self.edge3d_inside(surface_intersections[0]) and conical_face.edge3d_inside(surface_intersections[0]): contour3d = design3d.wires.Contour3D( [design3d.edges.FullArcEllipse3D.from_curve(surface_intersections[0])] ) return [contour3d] intersections_points = self.face_intersections_outer_contour(conical_face) for point in conical_face.face_intersections_outer_contour(self): if not point.in_list(intersections_points): intersections_points.append(point) face_intersections = [] for primitive in surface_intersections: points_on_primitive = [] for point in intersections_points: if primitive.point_belongs(point): points_on_primitive.append(point) if not points_on_primitive: continue points_on_primitive = primitive.sort_points_along_curve(points_on_primitive) if isinstance(primitive, design3d_curves.ClosedCurve): points_on_primitive = points_on_primitive + [points_on_primitive[0]] for point1, point2 in zip(points_on_primitive[:-1], points_on_primitive[1:]): if point1 == point2: continue edge = primitive.trim(point1, point2) if self.edge3d_inside(edge) and conical_face.edge3d_inside(edge): face_intersections.append(design3d.wires.Wire3D([edge])) return face_intersections
[docs] def toroidalface_intersections(self, toroidal_face): """ Calculates the intersections between a plane face 3D and Conical Face3D. :param toroidal_face: the Toroidal Face 3D to verify intersections with Plane Face 3D. :return: list of intersecting wires. """ surface_intersections = self.surface3d.surface_intersections(toroidal_face.surface3d) intersections_points = self.face_intersections_outer_contour(toroidal_face) for point in toroidal_face.face_intersections_outer_contour(self): if not point.in_list(intersections_points): intersections_points.append(point) face_intersections = [] for primitive in surface_intersections: points_on_primitive = [] for point in intersections_points: if primitive.point_belongs(point, 1e-5): points_on_primitive.append(point) if not points_on_primitive: continue points_on_primitive = primitive.sort_points_along_curve(points_on_primitive) if primitive.periodic: points_on_primitive = points_on_primitive + [points_on_primitive[0]] for point1, point2 in zip(points_on_primitive[:-1], points_on_primitive[1:]): edge = primitive.trim(point1, point2) if self.edge3d_inside(edge) and toroidal_face.edge3d_inside(edge, 1e-4): face_intersections.append(design3d.wires.Wire3D([edge])) return face_intersections
[docs] def planeface_minimum_distance(self, planeface: "PlaneFace3D", return_points: bool = False): """ Gets the minimal distance from another PlaneFace3D. :param planeface: Another PlaneFace3D instance to calculate the minimum distance. :type planeface: PlaneFace3D :param return_points: If True, returns a tuple containing the two points that give the minimum distance. :type return_points: bool, optional :return: If return_points is False, returns the minimum distance between the two plane faces. If return_points is True, returns a tuple containing the two points that give the minimum distance. :rtype: float or tuple(float, Tuple3D, Tuple3D) """ dist, point1, point2 = self.minimum_distance_points_plane(planeface, return_points=True) if not return_points: return dist return dist, point1, point2
[docs] def is_adjacent(self, face2: Face3D): """ Verifies if two plane faces are adjacent to eachother. :param face2: other face. :return: True if adjacent, False otherwise. """ contour1 = self.outer_contour3d.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) contour2 = face2.outer_contour3d.to_2d( self.surface3d.frame.origin, self.surface3d.frame.u, self.surface3d.frame.v ) if contour1.is_sharing_primitives_with(contour2, False): return True return False
[docs] @staticmethod def merge_faces(list_coincident_faces: List[Face3D]): """Merges faces from a list of faces in the same plane, if any are adjacent to one another.""" list_coincident_faces = sorted(list_coincident_faces, key=lambda face_: face_.area()) current_face = list_coincident_faces[0] list_coincident_faces.remove(current_face) list_merged_faces = [] while True: for face in list_coincident_faces: if current_face.outer_contour3d.is_sharing_primitives_with(face.outer_contour3d): merged_contours = current_face.outer_contour3d.merge_with(face.outer_contour3d) merged_contours2d = [ contour.to_2d(face.surface3d.frame.origin, face.surface3d.frame.u, face.surface3d.frame.v) for contour in merged_contours ] merged_contours2d = sorted(merged_contours2d, key=lambda contour: contour.area(), reverse=True) if not merged_contours2d and current_face.outer_contour3d.is_superposing(face.outer_contour3d): merged_contours2d = [current_face.surface2d.outer_contour] new_outer_contour = merged_contours2d[0] inner_contours = [ contour.to_2d(face.surface3d.frame.origin, face.surface3d.frame.u, face.surface3d.frame.v) for contour in current_face.inner_contours3d ] inner_contours += merged_contours2d[1:] + face.surface2d.inner_contours new_face = PlaneFace3D(face.surface3d, surfaces.Surface2D(new_outer_contour, inner_contours)) current_face = new_face list_coincident_faces.remove(face) break if current_face.face_inside(face): list_coincident_faces.remove(face) break new_inner_contours = [] inner_contour_merged = False for inner_contour3d in face.inner_contours3d: if current_face.outer_contour3d.is_sharing_primitives_with(inner_contour3d): merged_inner_contours = current_face.outer_contour3d.merge_with(inner_contour3d) if len(merged_inner_contours) >= 2: raise NotImplementedError new_inner_contours.extend(merged_inner_contours) inner_contour_merged = True if inner_contour_merged: list_coincident_faces.remove(face) inner_contours2d = [ inner_contour.to_2d( face.surface3d.frame.origin, face.surface3d.frame.u, face.surface3d.frame.v ) for inner_contour in new_inner_contours ] current_face = PlaneFace3D( face.surface3d, surfaces.Surface2D(face.surface2d.outer_contour, inner_contours2d) ) break else: list_merged_faces.append(current_face) if not list_coincident_faces: break current_face = list_coincident_faces[0] list_coincident_faces.remove(current_face) return list_merged_faces
[docs] def cut_by_coincident_face(self, face): """ Cuts face1 with another coincident face2. :param face: a face 3d. :type face: Face3D. :return: a list of faces 3d. :rtype: List[Face3D]. """ if not self.surface3d.is_coincident(face.surface3d): raise ValueError("The faces are not coincident") if self.face_inside(face): return self.divide_face([face.surface2d.outer_contour]) outer_contour_1 = self.surface2d.outer_contour outer_contour_2 = self.surface3d.contour3d_to_2d(face.outer_contour3d) if face.face_inside(self) and not outer_contour_1.intersection_points(outer_contour_2): return self.divide_face(face.surface2d.inner_contours) inner_contours = self.surface2d.inner_contours inner_contours.extend([self.surface3d.contour3d_to_2d(contour) for contour in face.inner_contours3d]) contours = outer_contour_1.cut_by_wire(outer_contour_2) list_surfaces = [] for contour in contours: inners = [] for inner_c in inner_contours: if contour.is_inside(inner_c): inners.append(inner_c) list_surfaces.append(surfaces.Surface2D(contour, inners)) return [self.__class__(self.surface3d, surface2d) for surface2d in list_surfaces]
[docs] def check_inner_contours(self, face): """ Checks face inner contours. """ c_inners_1 = self.surface2d.inner_contours c_inners_2 = [self.surface3d.contour3d_to_2d(inner) for inner in face.inner_contours3d] inside = set() for inner_contour1 in c_inners_1: for inner_contour2 in c_inners_2: if inner_contour1.is_superposing(inner_contour2): inside.add(False) else: inside.add(inner_contour2.is_inside(inner_contour1)) return inside
[docs] @staticmethod def update_faces_with_divided_faces(divided_faces, face2_2, used, list_faces): """ Update divided faces from project_faces. """ for d_face in divided_faces: if d_face.outer_contour3d.is_superposing(face2_2.outer_contour3d): if face2_2.surface2d.inner_contours: divided_faces_d_face = [] for inner in face2_2.surface2d.inner_contours: if True in [ ( ( (abs(inner_d.area() - inner.area()) < 1e-6) and inner.center_of_mass().is_close(inner_d.center_of_mass()) ) or inner_d.is_inside(inner) ) for inner_d in d_face.surface2d.inner_contours ]: divided_faces_d_face = ["", d_face] continue divided_faces_d_face = d_face.divide_face([inner]) divided_faces_d_face.sort(key=lambda x: x.area()) list_faces.append(divided_faces_d_face[0]) d_face = divided_faces_d_face[1] if divided_faces_d_face: list_faces.append(divided_faces_d_face[1]) else: list_faces.append(d_face) else: used.append(d_face) return used, list_faces
[docs] def project_faces(self, faces): """ Divide self based on the faces outer, and inner contours. :param faces: DESCRIPTION :type faces: TYPE :return: DESCRIPTION :rtype: TYPE """ used_faces, list_faces = {}, [] for face2 in faces: if self.surface3d.is_coincident(face2.surface3d): contour1 = self.surface2d.outer_contour contour2 = self.surface3d.contour3d_to_2d(face2.outer_contour3d) inside = self.check_inner_contours(face2) if contour1.is_overlapping(contour2) or (contour1.is_inside(contour2) or True in inside): if self in used_faces: faces_1, face2_2 = used_faces[self][:], face2 else: faces_1, face2_2 = [self], face2 used = [] for face1_1 in faces_1: plane3d = face1_1.surface3d s2d = surfaces.Surface2D( outer_contour=plane3d.contour3d_to_2d(face2_2.outer_contour3d), inner_contours=[plane3d.contour3d_to_2d(contour) for contour in face2_2.inner_contours3d], ) face2_2 = PlaneFace3D(surface3d=plane3d, surface2d=s2d) divided_faces = face1_1.cut_by_coincident_face(face2_2) used, list_faces = self.update_faces_with_divided_faces( divided_faces, face2_2, used, list_faces ) used_faces[self] = used try: if isinstance(used_faces[self], list): list_faces.extend(used_faces[self]) else: list_faces.append(used_faces[self]) except KeyError: list_faces.append(self) return list_faces
[docs] def get_geo_lines(self, tag: int, line_loop_tag: List[int]): """ Gets the lines that define a PlaneFace3D in a .geo file. """ return "Plane Surface(" + str(tag) + ") = {" + str(line_loop_tag)[1:-1] + "};"
[docs] @classmethod def from_surface_rectangular_cut(cls, plane3d, x1: float, x2: float, y1: float, y2: float, name: str = ""): """ Cut a rectangular piece of the Plane3D object and return a PlaneFace3D object. """ point1 = design3d.Point2D(x1, y1) point2 = design3d.Point2D(x2, y1) point3 = design3d.Point2D(x2, y2) point4 = design3d.Point2D(x1, y2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) surface = surfaces.Surface2D(outer_contour, []) return cls(plane3d, surface, name)
[docs] class PeriodicalFaceMixin: """ Abstract class for mutualizing methods for faces constructed on periodic surfaces. """
[docs] def point_belongs(self, point3d: design3d.Point3D, tol: float = 1e-6) -> bool: """ Checks if a 3D point lies on the face. :param point3d: The 3D point to be checked. :type point3d: design3d.Point3D :param tol: Tolerance for the check. :type tol: float, optional :return: True if the point is on the ConicalFace3D, False otherwise. :rtype: bool """ if not self.surface3d.point_belongs(point3d, tol): return False point2d = self.surface3d.point3d_to_2d(point3d) u_min, u_max, v_min, v_max = self.surface2d.bounding_rectangle().bounds() if self.surface3d.x_periodicity: if point2d.x < u_min - tol: point2d.x += self.surface3d.x_periodicity elif point2d.x > u_max + tol: point2d.x -= self.surface3d.x_periodicity if self.surface3d.y_periodicity: if point2d.y < v_min - tol: point2d.y += self.surface3d.y_periodicity elif point2d.y > v_max + tol: point2d.y -= self.surface3d.y_periodicity return self.surface2d.point_belongs(point2d, tol)
[docs] def face_inside(self, face2, abs_tol: float = 1e-6): """ Verifies if a face is inside another one. It returns True if face2 is inside or False if the opposite. """ if self.surface3d.frame.is_close(face2.surface3d.frame): return parametric_face_inside(self, face2, abs_tol) return super().face_inside(face2, abs_tol)
[docs] class Triangle3D(PlaneFace3D): """ Defines a Triangle3D class. :param point1: The first point. :type point1: design3d.Point3D. :param point2: The second point. :type point2: design3d.Point3D. :param point3: The third point. :type point3: design3d.Point3D. """ def __init__( self, point1: design3d.Point3D, point2: design3d.Point3D, point3: design3d.Point3D, alpha=1, color=None, name: str = "", ): self.point1 = point1 self.point2 = point2 self.point3 = point3 self.points = [self.point1, self.point2, self.point3] self.color = color self.alpha = alpha self.name = name self._surface3d = None self._surface2d = None self._bbox = None self._outer_contour3d = None self._inner_contours3d = None # self.bounding_box = self._bounding_box() PlaneFace3D.__init__(self, self.surface3d, self.surface2d) def _data_hash(self): """ Using point approx hash to speed up. """ return self.point1.approx_hash() + self.point2.approx_hash() + self.point3.approx_hash() def _data_eq(self, other_object): if other_object.__class__.__name__ != self.__class__.__name__: return False self_set = {self.point1, self.point2, self.point3} other_set = {other_object.point1, other_object.point2, other_object.point3} if self_set != other_set: return False return True
[docs] def get_bounding_box(self): """General method to get the bounding box.""" return design3d.core.BoundingBox.from_points([self.point1, self.point2, self.point3])
@property def surface3d(self): """Gets the plane on which the triangle is contained.""" if self._surface3d is None: self._surface3d = surfaces.Plane3D.from_3_points(self.point1, self.point2, self.point3) return self._surface3d @surface3d.setter def surface3d(self, new_surface3d): """Sets the plane on which the triangle is contained.""" self._surface3d = new_surface3d @property def surface2d(self): """Boundary representation of the face.""" if self._surface2d is None: plane3d = self.surface3d contour3d = design3d.wires.Contour3D( [ d3de.LineSegment3D(self.point1, self.point2), d3de.LineSegment3D(self.point2, self.point3), d3de.LineSegment3D(self.point3, self.point1), ] ) contour2d = contour3d.to_2d(plane3d.frame.origin, plane3d.frame.u, plane3d.frame.v) self._surface2d = surfaces.Surface2D(outer_contour=contour2d, inner_contours=[]) return self._surface2d @surface2d.setter def surface2d(self, new_surface2d): """Sets the boundary representation of the face.""" self._surface2d = new_surface2d
[docs] def to_dict(self, *args, **kwargs): """ Creates a Dictionary with the object's instance attributes. """ dict_ = { "object_class": "design3d.faces.Triangle3D", "point1": self.point1.to_dict(), "point2": self.point2.to_dict(), "point3": self.point3.to_dict(), } if self.name: dict_["name"] = self.name return dict_
[docs] @classmethod def dict_to_object(cls, dict_, *args, **kwargs): """ Create a Triangle3D object from a dictionary representation. This class method takes a dictionary containing the necessary data for creating a Triangle3D object and returns an instance of the Triangle3D class. It expects the dictionary to have the following keys: :param cls: The Triangle3D class itself (automatically passed). :param dict_: A dictionary containing the required data for object creation. :param args: Additional positional arguments (if any). :param kwargs: Additional keyword arguments (if any). :return: Triangle3D: An instance of the Triangle3D class created from the provided dictionary. """ point1 = design3d.Point3D.dict_to_object(dict_["point1"]) point2 = design3d.Point3D.dict_to_object(dict_["point2"]) point3 = design3d.Point3D.dict_to_object(dict_["point3"]) return cls(point1, point2, point3, dict_.get("name", ""))
[docs] def area(self) -> float: """ Calculates the area for the Triangle3D. :return: area triangle. :rtype: float. Formula explained here: https://www.triangle-calculator.com/?what=vc """ a = self.point1.point_distance(self.point2) b = self.point2.point_distance(self.point3) c = self.point3.point_distance(self.point1) semi_perimeter = (a + b + c) / 2 try: # Area with Heron's formula area = math.sqrt(semi_perimeter * (semi_perimeter - a) * (semi_perimeter - b) * (semi_perimeter - c)) except ValueError: area = 0 return area
[docs] def height(self): """ Gets Triangle height. """ # Formula explained here: https://www.triangle-calculator.com/?what=vc # Basis = vector point1 to point 2d return 2 * self.area() / self.point1.point_distance(self.point2)
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new Triangle3D. :param frame: frame used. :param side: 'old' or 'new'. """ np1 = self.point1.frame_mapping(frame, side) np2 = self.point2.frame_mapping(frame, side) np3 = self.point3.frame_mapping(frame, side) return self.__class__(np1, np2, np3, self.name)
[docs] def copy(self, deep=True, memo=None): """Returns a copy of the Triangle3D.""" return Triangle3D(self.point1.copy(), self.point2.copy(), self.point3.copy(), self.name)
[docs] def triangulation(self): """Computes the triangulation of the Triangle3D, basically returns itself.""" return d3dd.Mesh3D(np.array([self.point1, self.point2, self.point3]), np.array([[0, 1, 2]], dtype=np.int8))
[docs] def translation(self, offset: design3d.Vector3D): """ Plane3D translation. :param offset: translation vector. :return: A new translated Plane3D. """ new_point1 = self.point1.translation(offset) new_point2 = self.point2.translation(offset) new_point3 = self.point3.translation(offset) new_triangle = Triangle3D(new_point1, new_point2, new_point3, self.alpha, self.color, self.name) return new_triangle
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ Triangle3D rotation. :param center: rotation center. :param axis: rotation axis. :param angle: angle rotation. :return: a new rotated Triangle3D. """ new_point1 = self.point1.rotation(center, axis, angle) new_point2 = self.point2.rotation(center, axis, angle) new_point3 = self.point3.rotation(center, axis, angle) new_triangle = Triangle3D(new_point1, new_point2, new_point3, self.alpha, self.color, self.name) return new_triangle
[docs] @staticmethod def get_subdescription_points(new_points, resolution, max_length): """Gets sub-description points.""" vector = (new_points[0] - new_points[1]).unit_vector() points_01 = [new_points[1]] points_01 += [ new_points[1] + vector * min(k * resolution, max_length) for k in range(1, int(max_length / resolution) + 2) ] vector, length_2_1 = (new_points[2] - new_points[1]).unit_vector(), new_points[2].point_distance(new_points[1]) points_in = [] for p0_1 in points_01: point_on_2_1 = new_points[1] + vector * min( points_01[0].point_distance(p0_1) * length_2_1 / max_length, length_2_1 ) length_2_0 = point_on_2_1.point_distance(p0_1) nb_int = int(length_2_0 / resolution) + 2 if nb_int == 2: points_in.append(point_on_2_1) else: vector_2_0 = (point_on_2_1 - p0_1).unit_vector() step_in = length_2_0 / (nb_int - 1) points_in += [ p0_1 + vector_2_0 * min(i * step_in, length_2_0) for i in range(nb_int) if min(i * step_in, length_2_0) != 0 ] points = points_01 + points_in unique_points = list(set(points)) return unique_points
[docs] def subdescription(self, resolution=0.01): """ Returns a list of Point3D with resolution as max between Point3D. """ lengths = [ self.points[0].point_distance(self.points[1]), self.points[1].point_distance(self.points[2]), self.points[2].point_distance(self.points[0]), ] max_length = max(lengths) if max_length <= resolution: return self.points pos_length_max = lengths.index(max_length) new_points = [self.points[-3 + pos_length_max + k] for k in range(3)] return self.get_subdescription_points(new_points, resolution, max_length)
[docs] def subdescription_to_triangles(self, resolution=0.01): """ Returns a list of Triangle3D with resolution as max length of sub triangles side. """ sub_triangles = [self.points] while True: triangles = [] for subtri in sub_triangles: lengths = [ subtri[0].point_distance(subtri[1]), subtri[1].point_distance(subtri[2]), subtri[2].point_distance(subtri[0]), ] max_length = max(lengths) if max_length > resolution: pos_length_max = lengths.index(max_length) pt_mid = (subtri[-3 + pos_length_max] + subtri[-3 + pos_length_max + 1]) / 2 triangles.extend( [ [subtri[-3 + pos_length_max], pt_mid, subtri[-3 + pos_length_max + 2]], [subtri[-3 + pos_length_max + 1], pt_mid, subtri[-3 + pos_length_max + 2]], ] ) else: triangles.append(subtri) if len(sub_triangles) == len(triangles): break sub_triangles = triangles return [Triangle3D(subtri[0], subtri[1], subtri[2]) for subtri in sub_triangles]
[docs] def middle(self): """ Gets the middle point of the face: center of gravity. """ return (self.point1 + self.point2 + self.point3) / 3
[docs] def normal(self): """ Get the normal vector to the face. Returns ------- normal to the face """ normal = self.surface3d.frame.w normal = normal.unit_vector() return normal
[docs] def triangle_minimum_distance(self, triangle_face, return_points=False): """ Gets the minimum distance between two triangle. """ return self.planeface_minimum_distance(triangle_face, return_points)
[docs] class CylindricalFace3D(PeriodicalFaceMixin, Face3D): """ Defines a CylindricalFace3D class. :param surface3d: a cylindrical surface 3d. :type surface3d: CylindricalSurface3D. :param surface2d: a 2d surface to define the cylindrical face. :type surface2d: Surface2D. :Example: contours 2d is rectangular and will create a classic cylinder with x= 2*pi*radius, y=h """ min_x_density = 5 min_y_density = 1 def __init__(self, surface3d: surfaces.CylindricalSurface3D, surface2d: surfaces.Surface2D, name: str = ""): self.radius = surface3d.radius self.center = surface3d.frame.origin self.normal = surface3d.frame.w Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def copy(self, deep=True, memo=None): """Returns a copy of the CylindricalFace3D.""" return CylindricalFace3D(self.surface3d.copy(deep, memo), self.surface2d.copy(), self.name)
[docs] def triangulation_lines(self, angle_resolution=5): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ theta_min, theta_max, zmin, zmax = self.surface2d.bounding_rectangle().bounds() delta_theta = theta_max - theta_min nlines = math.ceil(delta_theta * angle_resolution) lines = [] for i in range(nlines): theta = theta_min + (i + 1) / (nlines + 1) * delta_theta lines.append(design3d_curves.Line2D(design3d.Point2D(theta, zmin), design3d.Point2D(theta, zmax))) return lines, []
[docs] def parametrized_grid_size(self, angle_resolution, z_resolution): """ Gets size for parametrized grid. :param angle_resolution: angle resolution. :param z_resolution: z resolution. :return: number of points in x and y. """ theta_min, theta_max, zmin, zmax = self.surface2d.bounding_rectangle().bounds() delta_theta = theta_max - theta_min number_points_x = max(angle_resolution, int(delta_theta * angle_resolution)) delta_z = zmax - zmin number_points_y = min(int(delta_z * z_resolution), 20) return number_points_x, number_points_y
[docs] def adjacent_direction(self, other_face3d): """ Find out in which direction the faces are adjacent. :param other_face3d: The face to evaluation. :type other_face3d: design3d.faces.CylindricalFace3D """ contour1 = self.outer_contour3d contour2 = other_face3d.outer_contour3d point1, point2 = contour1.shared_primitives_extremities(contour2) coord = point1 - point2 coord = [abs(coord.x), abs(coord.y)] if coord.index(max(coord)) == 0: return "x" return "y"
[docs] def get_geo_lines(self, tag: int, line_loop_tag: List[int]): """ Gets the lines that define a CylindricalFace3D in a .geo file. """ return "Surface(" + str(tag) + ") = {" + str(line_loop_tag)[1:-1] + "};"
[docs] def arc_inside(self, arc: d3de.Arc3D): """ Verifies if Arc3D is inside a CylindricalFace3D. :param arc: Arc3D to be verified. :return: True if it is inside, False otherwise. """ if not math.isclose(abs(arc.circle.frame.w.dot(self.surface3d.frame.w)), 1.0, abs_tol=1e-6): return False if not math.isclose(self.radius, arc.circle.radius, abs_tol=1e-6): return False return self.arcellipse_inside(arc)
[docs] def arcellipse_inside(self, arcellipse: d3de.ArcEllipse3D): """ Verifies if ArcEllipse3D is inside a CylindricalFace3D. :param arcellipse: ArcEllipse3D to be verified. :return: True if it is inside, False otherwise. """ for point in [arcellipse.start, arcellipse.middle_point(), arcellipse.end]: if not self.point_belongs(point): return False return True
[docs] def planeface_intersections(self, planeface: PlaneFace3D): """ Finds intersections with the given plane face. :param planeface: Plane face to evaluate the intersections. """ planeface_intersections = planeface.cylindricalface_intersections(self) return planeface_intersections
[docs] @classmethod def from_surface_rectangular_cut( cls, cylindrical_surface, theta1: float, theta2: float, param_z1: float, param_z2: float, name: str = "" ): """ Cut a rectangular piece of the CylindricalSurface3D object and return a CylindricalFace3D object. """ if theta1 == theta2: theta2 += design3d.TWO_PI point1 = design3d.Point2D(theta1, param_z1) point2 = design3d.Point2D(theta2, param_z1) point3 = design3d.Point2D(theta2, param_z2) point4 = design3d.Point2D(theta1, param_z2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) surface2d = surfaces.Surface2D(outer_contour, []) return cls(cylindrical_surface, surface2d, name)
[docs] def neutral_fiber(self): """ Returns the faces' neutral fiber. """ _, _, zmin, zmax = self.surface2d.outer_contour.bounding_rectangle.bounds() point1 = self.surface3d.frame.origin + self.surface3d.frame.w * zmin point2 = self.surface3d.frame.origin + self.surface3d.frame.w * zmax return design3d.wires.Wire3D([d3de.LineSegment3D(point1, point2)])
[docs] class ToroidalFace3D(PeriodicalFaceMixin, Face3D): """ Defines a ToroidalFace3D class. :param surface3d: a toroidal surface 3d. :type surface3d: ToroidalSurface3D. :param surface2d: a 2d surface to define the toroidal face. :type surface2d: Surface2D. :Example: contours 2d is rectangular and will create a classic tore with x:2*pi, y:2*pi x is for exterior, and y for the circle to revolute points = [pi, 2*pi] for a half tore """ min_x_density = 5 min_y_density = 1 face_tolerance = 1e-3 def __init__(self, surface3d: surfaces.ToroidalSurface3D, surface2d: surfaces.Surface2D, name: str = ""): # self.toroidalsurface3d = toroidalsurface3d self.center = surface3d.frame.origin self.normal = surface3d.frame.w theta_min, theta_max, phi_min, phi_max = surface2d.outer_contour.bounding_rectangle.bounds() self.theta_min = theta_min self.theta_max = theta_max self.phi_min = phi_min self.phi_max = phi_max # contours3d = [self.toroidalsurface3d.contour2d_to_3d(c)\ # for c in [outer_contour2d]+inners_contours2d] Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def copy(self, deep=True, memo=None): """Returns a copy of the ToroidalFace3D.""" return ToroidalFace3D(self.surface3d.copy(deep, memo), self.surface2d.copy(), self.name)
[docs] @staticmethod def points_resolution(line, pos, resolution): # With a resolution wished """ Legacy?. """ points = [line.points[0]] limit = line.points[1].vector[pos] start = line.points[0].vector[pos] vec = [0, 0] vec[pos] = start echelon = [line.points[0].vector[0] - vec[0], line.points[0].vector[1] - vec[1]] flag = start + resolution while flag < limit: echelon[pos] = flag flag += resolution points.append(design3d.Point2D(echelon)) points.append(line.points[1]) return points
[docs] def triangulation_lines(self, angle_resolution=5): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ theta_min, theta_max, phi_min, phi_max = self.surface2d.bounding_rectangle().bounds() delta_theta = theta_max - theta_min nlines_x = int(delta_theta * angle_resolution) lines_x = [] for i in range(nlines_x): theta = theta_min + (i + 1) / (nlines_x + 1) * delta_theta lines_x.append(design3d_curves.Line2D(design3d.Point2D(theta, phi_min), design3d.Point2D(theta, phi_max))) delta_phi = phi_max - phi_min nlines_y = int(delta_phi * angle_resolution) lines_y = [] for i in range(nlines_y): phi = phi_min + (i + 1) / (nlines_y + 1) * delta_phi lines_y.append(design3d_curves.Line2D(design3d.Point2D(theta_min, phi), design3d.Point2D(theta_max, phi))) return lines_x, lines_y
[docs] def grid_size(self): """ Specifies an adapted size of the discretization grid used in face triangulation. """ theta_angle_resolution = 10 phi_angle_resolution = 20 theta_min, theta_max, phi_min, phi_max = self.surface2d.bounding_rectangle().bounds() delta_theta = theta_max - theta_min number_points_x = math.ceil(delta_theta / math.radians(theta_angle_resolution)) delta_phi = phi_max - phi_min number_points_y = math.ceil(delta_phi / math.radians(phi_angle_resolution)) return number_points_x, number_points_y
[docs] @classmethod def from_surface_rectangular_cut( cls, toroidal_surface3d, theta1: float = 0.0, theta2: float = design3d.TWO_PI, phi1: float = 0.0, phi2: float = design3d.TWO_PI, name: str = "", ): """ Cut a rectangular piece of the ToroidalSurface3D object and return a ToroidalFace3D object. :param toroidal_surface3d: surface 3d, :param theta1: Start angle of the cut in theta direction. :param theta2: End angle of the cut in theta direction. :param phi1: Start angle of the cut in phi direction. :param phi2: End angle of the cut in phi direction. :param name: (optional) Name of the returned ToroidalFace3D object. Defaults to "". :return: A ToroidalFace3D object created by cutting the ToroidalSurface3D object. :rtype: ToroidalFace3D """ if phi1 == phi2: phi2 += design3d.TWO_PI elif phi2 < phi1: phi2 += design3d.TWO_PI if theta1 == theta2: theta2 += design3d.TWO_PI elif theta2 < theta1: theta2 += design3d.TWO_PI point1 = design3d.Point2D(theta1, phi1) point2 = design3d.Point2D(theta2, phi1) point3 = design3d.Point2D(theta2, phi2) point4 = design3d.Point2D(theta1, phi2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) return cls(toroidal_surface3d, surfaces.Surface2D(outer_contour, []), name)
[docs] def neutral_fiber(self): """ Returns the faces' neutral fiber. """ theta_min, theta_max, _, _ = self.surface2d.outer_contour.bounding_rectangle.bounds() circle = design3d_curves.Circle3D(self.surface3d.frame, self.surface3d.tore_radius) point1, point2 = [ circle.center + circle.radius * math.cos(theta) * circle.frame.u + circle.radius * math.sin(theta) * circle.frame.v for theta in [theta_min, theta_max] ] return design3d.wires.Wire3D([circle.trim(point1, point2)])
[docs] def planeface_intersections(self, planeface: PlaneFace3D): """ Gets intersections between a Toroidal Face 3D and a Plane Face 3D. :param planeface: other plane face. :return: intersections. """ planeface_intersections = planeface.toroidalface_intersections(self) return planeface_intersections
[docs] class ConicalFace3D(PeriodicalFaceMixin, Face3D): """ Defines a ConicalFace3D class. :param surface3d: a conical surface 3d. :type surface3d: ConicalSurface3D. :param surface2d: a 2d surface to define the conical face. :type surface2d: Surface2D. """ def __init__(self, surface3d: surfaces.ConicalSurface3D, surface2d: surfaces.Surface2D, name: str = ""): Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def triangulation_lines(self, angle_resolution=5): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ theta_min, theta_max, zmin, zmax = self.surface2d.bounding_rectangle().bounds() delta_theta = theta_max - theta_min nlines = int(delta_theta * angle_resolution) lines_x = [] for i in range(nlines): theta = theta_min + (i + 1) / (nlines + 1) * delta_theta lines_x.append(design3d_curves.Line2D(design3d.Point2D(theta, zmin), design3d.Point2D(theta, zmax))) if zmin < 1e-9: delta_z = zmax - zmin lines_y = [ design3d_curves.Line2D( design3d.Point2D(theta_min, zmin + 0.1 * delta_z), design3d.Point2D(theta_max, zmin + 0.1 * delta_z) ) ] else: lines_y = [] return lines_x, lines_y
[docs] @classmethod def from_surface_rectangular_cut( cls, conical_surface3d, theta1: float, theta2: float, z1: float, z2: float, name: str = "" ): """ Cut a rectangular piece of the ConicalSurface3D object and return a ConicalFace3D object. """ if theta1 < 0: theta1, theta2 = theta1 + 2 * math.pi, theta2 + 2 * math.pi if theta1 == theta2: theta2 += design3d.TWO_PI point1 = design3d.Point2D(theta1, z1) point2 = design3d.Point2D(theta2, z1) point3 = design3d.Point2D(theta2, z2) point4 = design3d.Point2D(theta1, z2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) return cls(conical_surface3d, surfaces.Surface2D(outer_contour, []), name)
[docs] @classmethod def from_base_and_vertex( cls, conical_surface3d, contour: design3d.wires.Contour3D, vertex: design3d.Point3D, name: str = "" ): """ Returns the conical face defined by the contour of the base and the cone vertex. :param conical_surface3d: surface 3d. :param contour: Cone, contour base. :type contour: design3d.wires.Contour3D :type vertex: design3d.Point3D :param name: the name to inject in the new face :return: Conical face. :rtype: ConicalFace3D """ contour2d = conical_surface3d.contour3d_to_2d(contour) start_contour2d = contour2d.primitives[0].start end_contour2d = contour2d.primitives[-1].end linesegment2d_1 = d3de.LineSegment2D(end_contour2d, design3d.Point2D(end_contour2d.x, 0)) linesegment2d_2 = d3de.LineSegment2D(design3d.Point2D(end_contour2d.x, 0), design3d.Point2D(start_contour2d.x, 0)) linesegment2d_3 = d3de.LineSegment2D(design3d.Point2D(start_contour2d.x, 0), start_contour2d) primitives2d = contour2d.primitives + [linesegment2d_1, linesegment2d_2, linesegment2d_3] outer_contour2d = design3d.wires.Contour2D(primitives2d) surface2d = surfaces.Surface2D(outer_contour=outer_contour2d, inner_contours=[]) return cls(conical_surface3d, surface2d=surface2d, name=name)
[docs] def neutral_fiber(self): """ Returns the faces' neutral fiber. """ _, _, zmin, zmax = self.surface2d.outer_contour.bounding_rectangle.bounds() point1 = self.surface3d.frame.origin + self.surface3d.frame.w * zmin point2 = self.surface3d.frame.origin + self.surface3d.frame.w * zmax return design3d.wires.Wire3D([d3de.LineSegment3D(point1, point2)])
[docs] def circle_inside(self, circle: design3d_curves.Circle3D): """ Verifies if a circle 3D lies completely on the Conical face. :param circle: Circle to be verified. :return: True if circle inside face. False otherwise. """ if not math.isclose(abs(circle.frame.w.dot(self.surface3d.frame.w)), 1.0, abs_tol=1e-6): return False points = circle.discretization_points(number_points=10) for point in points: if not self.point_belongs(point): return False return True
[docs] class SphericalFace3D(PeriodicalFaceMixin, Face3D): """ Defines a SpehericalFace3D class. :param surface3d: a spherical surface 3d. :type surface3d: SphericalSurface3D. :param surface2d: a 2d surface to define the spherical face. :type surface2d: Surface2D. """ min_x_density = 5 min_y_density = 5 def __init__(self, surface3d: surfaces.SphericalSurface3D, surface2d: surfaces.Surface2D, name: str = ""): Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def triangulation_lines(self, angle_resolution=7): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ theta_min, theta_max, phi_min, phi_max = self.surface2d.bounding_rectangle().bounds() delta_theta = theta_max - theta_min nlines_x = int(delta_theta * angle_resolution) lines_x = [] for i in range(nlines_x): theta = theta_min + (i + 1) / (nlines_x + 1) * delta_theta lines_x.append(design3d_curves.Line2D(design3d.Point2D(theta, phi_min), design3d.Point2D(theta, phi_max))) delta_phi = phi_max - phi_min nlines_y = int(delta_phi * angle_resolution) lines_y = [] for i in range(nlines_y): phi = phi_min + (i + 1) / (nlines_y + 1) * delta_phi lines_y.append(design3d_curves.Line2D(design3d.Point2D(theta_min, phi), design3d.Point2D(theta_max, phi))) return lines_x, lines_y
[docs] def grid_size(self): """ Specifies an adapted size of the discretization grid used to calculate the grid_points. For the sphere the grid size is given in angle resolution in both theta and phi direction. """ return 10, 10
[docs] def grid_points(self, grid_size, polygon_data=None): """ Parametric tesselation points. """ if polygon_data: outer_polygon, inner_polygons = polygon_data else: outer_polygon, inner_polygons = self.get_face_polygons() u, v, u_size, _ = self._get_grid_axis(outer_polygon, grid_size) if not u or not v: return [] if inner_polygons: points = [] points_indexes_map = {} for j, v_j in enumerate(v): for i in range(u_size): if (j % 2 == 0 and i % 2 == 0) or (j % 2 != 0 and i % 2 != 0): points.append((u[i], v_j)) points_indexes_map[(i, j)] = len(points) - 1 points = np.array(points, dtype=np.float64) points = update_face_grid_points_with_inner_polygons(inner_polygons, [points, u, v, points_indexes_map]) else: points = np.array([(u[i], v_j) for j, v_j in enumerate(v) for i in range(u_size) if (j % 2 == 0 and i % 2 == 0) or (j % 2 != 0 and i % 2 != 0)], dtype=np.float64) points = self._update_grid_points_with_outer_polygon(outer_polygon, points) return points
@staticmethod def _get_grid_axis(outer_polygon, grid_size): """Helper function to grid_points.""" theta_min, theta_max, phi_min, phi_max = outer_polygon.bounding_rectangle.bounds() theta_resolution, phi_resolution = grid_size step_u = 0.5 * math.radians(theta_resolution) step_v = math.radians(phi_resolution) u_size = math.ceil((theta_max - theta_min) / step_u) v_size = math.ceil((phi_max - phi_min) / step_v) v_start = phi_min + step_v u = [theta_min + step_u * i for i in range(u_size)] v = [v_start + j * step_v for j in range(v_size - 1)] return u, v, u_size, v_size
[docs] @classmethod def from_surface_rectangular_cut( cls, spherical_surface, theta1: float = 0.0, theta2: float = design3d.TWO_PI, phi1: float = -0.5 * math.pi, phi2: float = 0.5 * math.pi, name="", ): """ Cut a rectangular piece of the SphericalSurface3D object and return a SphericalFace3D object. """ if phi1 == phi2: phi2 += design3d.TWO_PI elif phi2 < phi1: phi2 += design3d.TWO_PI if theta1 == theta2: theta2 += design3d.TWO_PI elif theta2 < theta1: theta2 += design3d.TWO_PI point1 = design3d.Point2D(theta1, phi1) point2 = design3d.Point2D(theta2, phi1) point3 = design3d.Point2D(theta2, phi2) point4 = design3d.Point2D(theta1, phi2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) return cls(spherical_surface, surfaces.Surface2D(outer_contour, []), name=name)
[docs] @classmethod def from_contours3d_and_rectangular_cut( cls, surface3d, contours: List[design3d.wires.Contour3D], point: design3d.Point3D, name: str = "" ): """ Face defined by contours and a point indicating the portion of the parametric domain that should be considered. :param surface3d: surface 3d. :param contours: Cone, contour base. :type contours: List[design3d.wires.Contour3D] :type point: design3d.Point3D :param name: the name to inject in the new face :return: Spherical face. :rtype: SphericalFace3D """ inner_contours = [] point1 = design3d.Point2D(-math.pi, -0.5 * math.pi) point2 = design3d.Point2D(math.pi, -0.5 * math.pi) point3 = design3d.Point2D(math.pi, 0.5 * math.pi) point4 = design3d.Point2D(-math.pi, 0.5 * math.pi) surface_rectangular_cut = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) contours2d = [surface3d.contour3d_to_2d(contour) for contour in contours] point2d = surface3d.point3d_to_2d(point) for contour in contours2d: if not contour.point_inside(point2d): inner_contours.append(contour) surface2d = surfaces.Surface2D(outer_contour=surface_rectangular_cut, inner_contours=inner_contours) return cls(surface3d, surface2d=surface2d, name=name)
[docs] class RuledFace3D(Face3D): """ A 3D face with a ruled surface. This class represents a 3D face with a ruled surface, which is a surface formed by straight lines connecting two input curves. It is a subclass of the `Face3D` class and inherits all of its attributes and methods. :param surface3d: The 3D ruled surface of the face. :type surface3d: `RuledSurface3D` :param surface2d: The 2D projection of the face onto the parametric domain (u, v). :type surface2d: `Surface2D` :param name: The name of the face. :type name: str :param color: The color of the face. :type color: tuple """ min_x_density = 50 min_y_density = 1 def __init__(self, surface3d: surfaces.RuledSurface3D, surface2d: surfaces.Surface2D, name: str = ""): Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def get_bounding_box(self): """ General method to get the bounding box. To be enhanced by restricting wires to cut """ points = [self.surface3d.point2d_to_3d(design3d.Point2D(i / 30, 0.0)) for i in range(31)] points.extend([self.surface3d.point2d_to_3d(design3d.Point2D(i / 30, 1.0)) for i in range(31)]) return design3d.core.BoundingBox.from_points(points)
[docs] def triangulation_lines(self, angle_resolution=10): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ xmin, xmax, ymin, ymax = self.surface2d.bounding_rectangle().bounds() delta_x = xmax - xmin nlines = int(delta_x * angle_resolution) lines = [] for i in range(nlines): x = xmin + (i + 1) / (nlines + 1) * delta_x lines.append(design3d_curves.Line2D(design3d.Point2D(x, ymin), design3d.Point2D(x, ymax))) return lines, []
[docs] def grid_size(self): """ Specifies an adapted size of the discretization grid used in face triangulation. """ angle_resolution = 10 xmin, xmax, _, _ = self.surface2d.bounding_rectangle().bounds() delta_x = xmax - xmin number_points_x = int(delta_x * angle_resolution) number_points_y = 0 return number_points_x, number_points_y
[docs] @classmethod def from_surface_rectangular_cut( cls, ruled_surface3d, x1: float = 0.0, x2: float = 1.0, y1: float = 0.0, y2: float = 1.0, name: str = "" ): """ Cut a rectangular piece of the RuledSurface3D object and return a RuledFace3D object. """ point1 = design3d.Point2D(x1, y1) point2 = design3d.Point2D(x2, y1) point3 = design3d.Point2D(x2, y2) point4 = design3d.Point2D(x1, y2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) surface2d = surfaces.Surface2D(outer_contour, []) return cls(ruled_surface3d, surface2d, name)
[docs] class ExtrusionFace3D(Face3D): """ A 3D face with a ruled surface. This class represents a 3D face with a ruled surface, which is a surface formed by straight lines connecting two input curves. It is a subclass of the `Face3D` class and inherits all of its attributes and methods. :param surface3d: The 3D ruled surface of the face. :type surface3d: `RuledSurface3D` :param surface2d: The 2D projection of the face onto the parametric domain (u, v). :type surface2d: `Surface2D` :param name: The name of the face. :type name: str """ min_x_density = 50 min_y_density = 1 def __init__(self, surface3d: surfaces.ExtrusionSurface3D, surface2d: surfaces.Surface2D, name: str = ""): Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] @classmethod def from_surface_rectangular_cut( cls, extrusion_surface3d: surfaces.ExtrusionSurface3D, x1: float = 0.0, x2: float = 0.0, y1: float = 0.0, y2: float = 1.0, name: str = "", ): """ Cut a rectangular piece of the ExtrusionSurface3D object and return a ExtrusionFace3D object. """ if not x2: x2 = extrusion_surface3d.edge.length() p1 = design3d.Point2D(x1, y1) p2 = design3d.Point2D(x2, y1) p3 = design3d.Point2D(x2, y2) p4 = design3d.Point2D(x1, y2) outer_contour = design3d.wires.Contour2D.from_points([p1, p2, p3, p4]) surface2d = surfaces.Surface2D(outer_contour, []) return cls(extrusion_surface3d, surface2d, name)
[docs] class RevolutionFace3D(Face3D): """ A 3D face with a ruled surface. This class represents a 3D face with a ruled surface, which is a surface formed by straight lines connecting two input curves. It is a subclass of the `Face3D` class and inherits all of its attributes and methods. :param surface3d: The 3D ruled surface of the face. :type surface3d: `RuledSurface3D` :param surface2d: The 2D projection of the face onto the parametric domain (u, v). :type surface2d: `Surface2D` :param name: The name of the face. :type name: str """ min_x_density = 50 min_y_density = 1 def __init__(self, surface3d: surfaces.RevolutionSurface3D, surface2d: surfaces.Surface2D, name: str = ""): Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def grid_size(self): """ Specifies an adapted size of the discretization grid used in face triangulation. """ angle_resolution = 10 number_points_y = self.get_edge_discretization_size(self.surface3d.edge) return angle_resolution, number_points_y
[docs] def grid_points(self, grid_size, polygon_data=None): """ Parametric tesselation points. """ if polygon_data: outer_polygon, inner_polygons = polygon_data else: outer_polygon, inner_polygons, _ = self.get_face_polygons() u, v, u_size, _ = self._get_grid_axis(outer_polygon, grid_size) if not u or not v: return [] if inner_polygons: points = [] points_indexes_map = {} for j, v_j in enumerate(v): for i in range(u_size): if (j % 2 == 0 and i % 2 == 0) or (j % 2 != 0 and i % 2 != 0): points.append((u[i], v_j)) points_indexes_map[(i, j)] = len(points) - 1 points = np.array(points, dtype=np.float64) points = update_face_grid_points_with_inner_polygons(inner_polygons, [points, u, v, points_indexes_map]) else: points = np.array([(u[i], v_j) for j, v_j in enumerate(v) for i in range(u_size) if (j % 2 == 0 and i % 2 == 0) or (j % 2 != 0 and i % 2 != 0)], dtype=np.float64) points = self._update_grid_points_with_outer_polygon(outer_polygon, points) return points
@staticmethod def _get_grid_axis(outer_polygon, grid_size): """Helper function to grid_points.""" u_min, u_max, v_min, v_max = outer_polygon.bounding_rectangle.bounds() angle_resolution, number_points_v = grid_size delta_x = u_max - u_min number_points_u = math.ceil(delta_x / math.radians(angle_resolution)) u_step = (u_max - u_min) / (number_points_u - 1) if number_points_u > 1 else (u_max - u_min) v_step = (v_max - v_min) / (number_points_v - 1) if number_points_v > 1 else (v_max - v_min) u = [u_min + i * u_step for i in range(number_points_u)] v = [v_min + i * v_step for i in range(number_points_v)] return u, v, number_points_u, number_points_v
[docs] @classmethod def from_surface_rectangular_cut( cls, revolution_surface3d, x1: float, x2: float, y1: float, y2: float, name: str = "" ): """ Cut a rectangular piece of the RevolutionSurface3D object and return a RevolutionFace3D object. """ point1 = design3d.Point2D(x1, y1) point2 = design3d.Point2D(x2, y1) point3 = design3d.Point2D(x2, y2) point4 = design3d.Point2D(x1, y2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) surface2d = surfaces.Surface2D(outer_contour, []) return cls(revolution_surface3d, surface2d, name)
[docs] def get_face_polygons(self): """Get face polygons.""" primitives_mapping = self.primitives_mapping xmin, xmax, ymin, ymax = self.surface2d.bounding_rectangle().bounds() delta_x = xmax - xmin delta_y = ymax - ymin number_points_x, number_points_y = self.grid_size() scale_factor = 1 if number_points_x > 1 and number_points_y > 1: scale_factor = 10 ** math.floor( math.log10((delta_x/(number_points_x - 1))/(delta_y/(number_points_y - 1)))) def get_polygon_points(primitives): points = [] for edge in primitives: edge3d = primitives_mapping.get(edge) number_points = self.get_edge_discretization_size(edge3d) edge_points = edge.discretization_points(number_points=number_points) if scale_factor != 1: for point in edge_points: point.y *= scale_factor points.extend(edge_points[:-1]) return points outer_polygon = design3d.wires.ClosedPolygon2D(get_polygon_points(self.surface2d.outer_contour.primitives)) inner_polygons = [design3d.wires.ClosedPolygon2D(get_polygon_points(inner_contour.primitives)) for inner_contour in self.surface2d.inner_contours] return outer_polygon, inner_polygons, scale_factor
[docs] def triangulation(self): """Triangulates the face.""" outer_polygon, inner_polygons, scale_factor = self.get_face_polygons() mesh2d = self.helper_to_mesh([outer_polygon, inner_polygons]) if mesh2d is None: return None if scale_factor != 1: mesh2d.vertices[:, 1] /= scale_factor return d3dd.Mesh3D(self.surface3d.parametric_points_to_3d(mesh2d.vertices), mesh2d.triangles)
[docs] class BSplineFace3D(Face3D): """ A 3D face with a B-spline surface. This class represents a 3D face with a B-spline surface, which is a smooth surface defined by a set of control points and knots. It is a subclass of the `Face3D` class and inherits all of its attributes and methods. :param surface3d: The 3D B-spline surface of the face. :type surface3d: `BSplineSurface3D` :param surface2d: The 2D projection of the face onto the parametric domain (u, v). :type surface2d: `Surface2D` :param name: The name of the face. :type name: str """ face_tolerance = 1e-5 def __init__(self, surface3d: surfaces.BSplineSurface3D, surface2d: surfaces.Surface2D, name: str = ""): Face3D.__init__(self, surface3d=surface3d, surface2d=surface2d, name=name) self._bbox = None
[docs] def get_bounding_box(self): """Creates a bounding box from the face mesh.""" try: number_points_x, number_points_y = self.grid_size() if number_points_x >= number_points_y: number_points_x, number_points_y = 5, 3 else: number_points_x, number_points_y = 3, 5 points_grid = self.grid_points([number_points_x, number_points_y]) points3d = [self.surface3d.point2d_to_3d(point) for point in points_grid] except ZeroDivisionError: points3d = [] if not points3d: return self.outer_contour3d.bounding_box return design3d.core.BoundingBox.from_bounding_boxes( [design3d.core.BoundingBox.from_points(points3d), self.outer_contour3d.bounding_box] ).scale(1.001)
[docs] def triangulation_lines(self, resolution=25): """ Specifies the number of subdivision when using triangulation by lines. (Old triangulation). """ u_min, u_max, v_min, v_max = self.surface2d.bounding_rectangle().bounds() delta_u = u_max - u_min nlines_x = int(delta_u * resolution) lines_x = [] for i in range(nlines_x): u = u_min + (i + 1) / (nlines_x + 1) * delta_u lines_x.append(design3d_curves.Line2D(design3d.Point2D(u, v_min), design3d.Point2D(u, v_max))) delta_v = v_max - v_min nlines_y = int(delta_v * resolution) lines_y = [] for i in range(nlines_y): v = v_min + (i + 1) / (nlines_y + 1) * delta_v lines_y.append(design3d_curves.Line2D(design3d.Point2D(v_min, v), design3d.Point2D(v_max, v))) return lines_x, lines_y
[docs] def grid_size(self): """ Specifies an adapted size of the discretization grid used in face triangulation. """ u_min, u_max, v_min, v_max = self.surface2d.bounding_rectangle().bounds() delta_u = u_max - u_min delta_v = v_max - v_min resolution_u = self.surface3d.nb_u resolution_v = self.surface3d.nb_v if resolution_u > resolution_v: number_points_x = int(delta_u * resolution_u) number_points_y = max(int(delta_v * resolution_v), int(number_points_x / 5)) else: number_points_y = int(delta_v * resolution_v) number_points_x = max(int(delta_u * resolution_u), int(number_points_y / 5)) return number_points_x, number_points_y
@staticmethod def _update_grid_points_with_outer_polygon(outer_polygon, grid_points): """Helper function to grid_points.""" # Find the indices where points_in_polygon is True (i.e., points inside the polygon) indices = np.where(outer_polygon.points_in_polygon(grid_points, include_edge_points=True) == 0)[0] grid_points = np.delete(grid_points, indices, axis=0) polygon_points = set(outer_polygon.points) points = [design3d.Point2D(*point) for point in grid_points if design3d.Point2D(*point) not in polygon_points] return points
[docs] def pair_with(self, other_bspline_face3d): """ Finds out how the uv parametric frames are located. It does it by comparing to each other and also how grid 3d can be defined respected to these directions. :param other_bspline_face3d: BSplineFace3D :type other_bspline_face3d: :class:`design3d.faces.BSplineFace3D` :return: corresponding_direction, grid2d_direction :rtype: Tuple[?, ?] """ adjacent_direction1, diff1, adjacent_direction2, diff2 = self.adjacent_direction(other_bspline_face3d) corresponding_directions = [] if (diff1 > 0 and diff2 > 0) or (diff1 < 0 and diff2 < 0): corresponding_directions.append(("+" + adjacent_direction1, "+" + adjacent_direction2)) else: corresponding_directions.append(("+" + adjacent_direction1, "-" + adjacent_direction2)) if adjacent_direction1 == "u" and adjacent_direction2 == "u": corresponding_directions, grid2d_direction = self.adjacent_direction_uu( other_bspline_face3d, corresponding_directions ) elif adjacent_direction1 == "v" and adjacent_direction2 == "v": corresponding_directions, grid2d_direction = self.adjacent_direction_vv( other_bspline_face3d, corresponding_directions ) elif adjacent_direction1 == "u" and adjacent_direction2 == "v": corresponding_directions, grid2d_direction = self.adjacent_direction_uv( other_bspline_face3d, corresponding_directions ) elif adjacent_direction1 == "v" and adjacent_direction2 == "u": corresponding_directions, grid2d_direction = self.adjacent_direction_vu( other_bspline_face3d, corresponding_directions ) return corresponding_directions, grid2d_direction
[docs] def adjacent_direction_uu(self, other_bspline_face3d, corresponding_directions): """Returns the side of the faces that are adjacent.""" extremities = self.extremities(other_bspline_face3d) start1, start2 = extremities[0], extremities[2] borders_points = [design3d.Point2D(0, 0), design3d.Point2D(1, 0), design3d.Point2D(1, 1), design3d.Point2D(0, 1)] # TODO: compute nearest_point in 'bounding_box points' instead of borders_points nearest_start1 = start1.nearest_point(borders_points) # nearest_end1 = end1.nearest_point(borders_points) nearest_start2 = start2.nearest_point(borders_points) # nearest_end2 = end2.nearest_point(borders_points) v1 = nearest_start1[1] v2 = nearest_start2[1] if v1 == 0 and v2 == 0: if corresponding_directions == [("+u", "-u")]: grid2d_direction = [["-x", "-y"], ["+x", "+y"]] else: grid2d_direction = [["+x", "-y"], ["+x", "+y"]] corresponding_directions.append(("+v", "-v")) elif v1 == 1 and v2 == 1: if corresponding_directions == [("+u", "-u")]: grid2d_direction = [["+x", "+y"], ["-x", "-y"]] else: grid2d_direction = [["+x", "+y"], ["+x", "-y"]] corresponding_directions.append(("+v", "-v")) elif v1 == 1 and v2 == 0: corresponding_directions.append(("+v", "+v")) grid2d_direction = [["+x", "+y"], ["+x", "+y"]] elif v1 == 0 and v2 == 1: corresponding_directions.append(("+v", "+v")) grid2d_direction = [["+x", "-y"], ["+x", "-y"]] return corresponding_directions, grid2d_direction
[docs] def adjacent_direction_vv(self, other_bspline_face3d, corresponding_directions): """Returns the side of the faces that are adjacent.""" extremities = self.extremities(other_bspline_face3d) start1, start2 = extremities[0], extremities[2] borders_points = [design3d.Point2D(0, 0), design3d.Point2D(1, 0), design3d.Point2D(1, 1), design3d.Point2D(0, 1)] # TODO: compute nearest_point in 'bounding_box points' instead of borders_points nearest_start1 = start1.nearest_point(borders_points) # nearest_end1 = end1.nearest_point(borders_points) nearest_start2 = start2.nearest_point(borders_points) # nearest_end2 = end2.nearest_point(borders_points) u1 = nearest_start1[0] u2 = nearest_start2[0] if u1 == 0 and u2 == 0: corresponding_directions.append(("+u", "-v")) grid2d_direction = [["-y", "-x"], ["-y", "+x"]] elif u1 == 1 and u2 == 1: if corresponding_directions == [("+v", "-v")]: grid2d_direction = [["+y", "+x"], ["-y", "-x"]] else: grid2d_direction = [["+y", "+x"], ["+y", "-x"]] corresponding_directions.append(("+u", "-u")) elif u1 == 0 and u2 == 1: corresponding_directions.append(("+u", "+u")) grid2d_direction = [["+y", "-x"], ["+y", "-x"]] elif u1 == 1 and u2 == 0: corresponding_directions.append(("+u", "+u")) grid2d_direction = [["+y", "+x"], ["+y", "+x"]] return corresponding_directions, grid2d_direction
[docs] def adjacent_direction_uv(self, other_bspline_face3d, corresponding_directions): """ Returns the sides that are adjacents to other BSpline face. """ extremities = self.extremities(other_bspline_face3d) start1, start2 = extremities[0], extremities[2] borders_points = [design3d.Point2D(0, 0), design3d.Point2D(1, 0), design3d.Point2D(1, 1), design3d.Point2D(0, 1)] # TODO: compute nearest_point in 'bounding_box points' instead of borders_points nearest_start1 = start1.nearest_point(borders_points) # nearest_end1 = end1.nearest_point(borders_points) nearest_start2 = start2.nearest_point(borders_points) # nearest_end2 = end2.nearest_point(borders_points) v1 = nearest_start1[1] u2 = nearest_start2[0] if v1 == 1 and u2 == 0: corresponding_directions.append(("+v", "+u")) grid2d_direction = [["+x", "+y"], ["+y", "+x"]] elif v1 == 0 and u2 == 1: corresponding_directions.append(("+v", "+u")) grid2d_direction = [["-x", "-y"], ["-y", "-x"]] elif v1 == 1 and u2 == 1: corresponding_directions.append(("+v", "-u")) grid2d_direction = [["+x", "+y"], ["-y", "-x"]] elif v1 == 0 and u2 == 0: corresponding_directions.append(("+v", "-u")) grid2d_direction = [["-x", "-y"], ["-y", "+x"]] return corresponding_directions, grid2d_direction
[docs] def adjacent_direction_vu(self, other_bspline_face3d, corresponding_directions): """ Returns the sides that are adjacents to other BSpline face. """ extremities = self.extremities(other_bspline_face3d) start1, start2 = extremities[0], extremities[2] borders_points = [design3d.Point2D(0, 0), design3d.Point2D(1, 0), design3d.Point2D(1, 1), design3d.Point2D(0, 1)] # TODO: compute nearest_point in 'bounding_box points' instead of borders_points nearest_start1 = start1.nearest_point(borders_points) # nearest_end1 = end1.nearest_point(borders_points) nearest_start2 = start2.nearest_point(borders_points) # nearest_end2 = end2.nearest_point(borders_points) u1 = nearest_start1[0] v2 = nearest_start2[1] if u1 == 1 and v2 == 0: corresponding_directions.append(("+u", "+v")) grid2d_direction = [["+y", "+x"], ["+x", "+y"]] elif u1 == 0 and v2 == 1: corresponding_directions.append(("+u", "+v")) grid2d_direction = [["-y", "-x"], ["+x", "-y"]] elif u1 == 0 and v2 == 0: corresponding_directions.append(("+u", "-v")) grid2d_direction = [["+y", "-x"], ["+x", "+y"]] elif u1 == 1 and v2 == 1: if corresponding_directions == [("+v", "-u")]: grid2d_direction = [["+y", "+x"], ["-x", "-y"]] else: grid2d_direction = [["+y", "+x"], ["+x", "-y"]] corresponding_directions.append(("+u", "-v")) return corresponding_directions, grid2d_direction
[docs] def extremities(self, other_bspline_face3d): """ Find points extremities for nearest edges of two faces. """ contour1 = self.outer_contour3d contour2 = other_bspline_face3d.outer_contour3d contour1_2d = self.surface2d.outer_contour contour2_2d = other_bspline_face3d.surface2d.outer_contour points1 = [prim.start for prim in contour1.primitives] points2 = [prim.start for prim in contour2.primitives] dis, ind = [], [] for point_ in points1: point = point_.nearest_point(points2) ind.append(points2.index(point)) dis.append(point_.point_distance(point)) dis_sorted = sorted(dis) shared = [] for k, point1 in enumerate(contour1.primitives): if dis_sorted[0] == dis_sorted[1]: indices = np.where(np.array(dis) == dis_sorted[0])[0] index1 = indices[0] index2 = indices[1] else: index1 = dis.index(dis_sorted[0]) index2 = dis.index(dis_sorted[1]) if (point1.start.is_close(points1[index1]) and point1.end.is_close(points1[index2])) or ( point1.end.is_close(points1[index1]) and point1.start.is_close(points1[index2]) ): shared.append(point1) i = k for k, prim2 in enumerate(contour2.primitives): if (prim2.start.is_close(points2[ind[index1]]) and prim2.end.is_close(points2[ind[index2]])) or ( prim2.end.is_close(points2[ind[index1]]) and prim2.start.is_close(points2[ind[index2]]) ): shared.append(prim2) j = k points = [contour2.primitives[j].start, contour2.primitives[j].end] if points.index(contour1.primitives[i].start.nearest_point(points)) == 1: start1 = contour1_2d.primitives[i].start end1 = contour1_2d.primitives[i].end start2 = contour2_2d.primitives[j].end end2 = contour2_2d.primitives[j].start else: start1 = contour1_2d.primitives[i].start end1 = contour1_2d.primitives[i].end start2 = contour2_2d.primitives[j].start end2 = contour2_2d.primitives[j].end return start1, end1, start2, end2
[docs] def adjacent_direction(self, other_bspline_face3d): """ Find directions (u or v) between two faces, in the nearest edges between them. """ start1, end1, start2, end2 = self.extremities(other_bspline_face3d) du1 = abs((end1 - start1)[0]) dv1 = abs((end1 - start1)[1]) if du1 < dv1: adjacent_direction1 = "v" diff1 = (end1 - start1)[1] else: adjacent_direction1 = "u" diff1 = (end1 - start1)[0] du2 = abs((end2 - start2)[0]) dv2 = abs((end2 - start2)[1]) if du2 < dv2: adjacent_direction2 = "v" diff2 = (end2 - start2)[1] else: adjacent_direction2 = "u" diff2 = (end2 - start2)[0] return adjacent_direction1, diff1, adjacent_direction2, diff2
[docs] def adjacent_direction_xy(self, other_face3d): """ Find out in which direction the faces are adjacent. :type other_face3d: design3d.faces.BSplineFace3D :return: adjacent_direction """ contour1 = self.outer_contour3d contour2 = other_face3d.outer_contour3d point1, point2 = contour1.shared_primitives_extremities(contour2) coord = point1 - point2 coord = [abs(coord.x), abs(coord.y)] if coord.index(max(coord)) == 0: return "x" return "y"
[docs] def merge_with(self, other_bspline_face3d): """ Merge two adjacent faces. :type: other_bspline_face3d : design3d.faces.BSplineFace3D :rtype: merged_face : design3d.faces.BSplineFace3D """ merged_surface = self.surface3d.merge_with(other_bspline_face3d.surface3d) contours = self.outer_contour3d.merge_with(other_bspline_face3d.outer_contour3d) contours.extend(self.inner_contours3d) contours.extend(other_bspline_face3d.inner_contours3d) merged_face = self.from_contours3d(merged_surface, contours) return merged_face
[docs] @classmethod def from_surface_rectangular_cut( cls, bspline_surface3d, u1: float = 0.0, u2: float = 1.0, v1: float = 0.0, v2: float = 1.0, name: str = "" ): """ Cut a rectangular piece of the BSplineSurface3D object and return a BSplineFace3D object. """ point1 = design3d.Point2D(u1, v1) point2 = design3d.Point2D(u2, v1) point3 = design3d.Point2D(u2, v2) point4 = design3d.Point2D(u1, v2) outer_contour = design3d.wires.Contour2D.from_points([point1, point2, point3, point4]) surface = surfaces.Surface2D(outer_contour, []) return BSplineFace3D(bspline_surface3d, surface, name)
[docs] def to_planeface3d(self, plane3d: surfaces.Plane3D = None): """ Converts a Bspline face 3d to a Plane face 3d (using or without a reference Plane3D). :param plane3d: A reference Plane3D, defaults to None :type plane3d: Plane3D, optional :return: A Plane face 3d. :rtype: PlaneFace3D """ if not plane3d: plane3d = self.surface3d.to_plane3d() surface2d = surfaces.Surface2D( outer_contour=plane3d.contour3d_to_2d(self.outer_contour3d), inner_contours=[plane3d.contour3d_to_2d(contour) for contour in self.inner_contours3d], ) return PlaneFace3D(surface3d=plane3d, surface2d=surface2d)
[docs] @staticmethod def approximate_with_arc(edge): """ Returns an arc that approximates the given edge. :param edge: curve to be approximated by an arc. :return: An arc if possible, otherwise None. """ if edge.start.is_close(edge.end): start = edge.point_at_abscissa(0.25 * edge.length()) interior = edge.point_at_abscissa(0.5 * edge.length()) end = edge.point_at_abscissa(0.75 * edge.length()) vector1 = interior - start vector2 = interior - end if vector1.is_colinear_to(vector2) or vector1.norm() == 0 or vector2.norm() == 0: return None return d3de.Arc3D.from_3_points(start, interior, end) interior = edge.point_at_abscissa(0.5 * edge.length()) vector1 = interior - edge.start vector2 = interior - edge.end if vector1.is_colinear_to(vector2) or vector1.norm() == 0 or vector2.norm() == 0: return None return d3de.Arc3D.from_3_points(edge.start, interior, edge.end)
[docs] def get_approximating_arc_parameters(self, curve_list): """ Approximates the given curves with arcs and returns the arcs, radii, and centers. :param curve_list: A list of curves to approximate. :type curve_list: list :returns: A tuple containing the radius and centers of the approximating arcs. :rtype: tuple """ radius = [] centers = [] for curve in curve_list: if curve.simplify.__class__.__name__ in ("Arc3D", "FullArc3D"): arc = curve.simplify else: arc = self.approximate_with_arc(curve) if arc: radius.append(arc.circle.radius) centers.append(arc.circle.center) return radius, centers
[docs] def neutral_fiber_points(self): """ Calculates the neutral fiber points of the face. :returns: The neutral fiber points if they exist, otherwise None. :rtype: Union[list, None] """ surface_curves = self.surface3d.surface_curves u_curves = surface_curves["u"] v_curves = surface_curves["v"] u_curves = [ primitive.simplify for primitive in u_curves if not isinstance(primitive.simplify, d3de.LineSegment3D) ] v_curves = [ primitive.simplify for primitive in v_curves if not isinstance(primitive.simplify, d3de.LineSegment3D) ] u_radius, u_centers = self.get_approximating_arc_parameters(u_curves) v_radius, v_centers = self.get_approximating_arc_parameters(v_curves) if not u_radius and not v_radius: return None if v_radius and not u_radius: return v_centers if u_radius and not v_radius: return u_centers u_mean = np.mean(u_radius) v_mean = np.mean(v_radius) if u_mean > v_mean: return v_centers if u_mean < v_mean: return u_centers return None
[docs] def neutral_fiber(self): """ Returns the faces' neutral fiber. """ neutral_fiber_points = self.neutral_fiber_points() is_line = False neutral_fiber = None if not neutral_fiber_points[0].is_close(neutral_fiber_points[-1]): neutral_fiber = d3de.LineSegment3D(neutral_fiber_points[0], neutral_fiber_points[-1]) is_line = all(neutral_fiber.point_belongs(point) for point in neutral_fiber_points) if not is_line: neutral_fiber = d3de.BSplineCurve3D.from_points_interpolation( neutral_fiber_points, min(self.surface3d.degree_u, self.surface3d.degree_v) ) umin, umax, d3din, d3dax = self.surface2d.outer_contour.bounding_rectangle.bounds() min_bound_u, max_bound_u, min_bound_v, max_bound_v = self.surface3d.domain if not math.isclose(umin, min_bound_u, rel_tol=0.01) or not math.isclose(d3din, min_bound_v, rel_tol=0.01): point3d_min = self.surface3d.point2d_to_3d(design3d.Point2D(umin, d3din)) point1 = neutral_fiber.point_projection(point3d_min)[0] else: point1 = neutral_fiber.start if not math.isclose(umax, max_bound_u, rel_tol=0.01) or not math.isclose(d3dax, max_bound_v, rel_tol=0.01): point3d_max = self.surface3d.point2d_to_3d(design3d.Point2D(umax, d3dax)) return design3d.wires.Wire3D([neutral_fiber.trim(point1, neutral_fiber.point_projection(point3d_max)[0])]) return design3d.wires.Wire3D([neutral_fiber.trim(point1, neutral_fiber.end)])
[docs] def linesegment_intersections(self, linesegment: d3de.LineSegment3D, abs_tol: float = 1e-6) -> List[design3d.Point3D]: """ Get intersections between a BSpline face 3d and a Line Segment 3D. :param linesegment: other linesegment. :param abs_tol: tolerance. :return: a list of intersections. """ return self.linesegment_intersections_approximation(linesegment, abs_tol)