Source code for design3d.utils.common_operations

"""
Concatenate common operation for two or more objects.

"""
import math
import random

import matplotlib
import matplotlib.pyplot as plt
import numpy as np
from scipy.optimize import least_squares
import scipy.integrate as scipy_integrate
from sklearn.cluster import DBSCAN

import design3d.core
from design3d.core import EdgeStyle


[docs] def plot_circle(circle, ax=None, edge_style: EdgeStyle = EdgeStyle()): """ Create a Matplotlib plot for a circle 2d or fullarc 2d. :param circle: circle to plot. :param ax: Matplotlib plot axis. :param edge_style: Edge Style to implement. :return: Matplotlib plot axis. """ if ax is None: _, ax = plt.subplots() x_min, x_max = circle.center[0] - circle.radius, circle.center[0] + circle.radius y_min, y_max = circle.center[1] - circle.radius, circle.center[1] + circle.radius ax.set_xlim(x_min, x_max) ax.set_ylim(y_min, y_max) if circle.radius > 0: ax.add_patch(matplotlib.patches.Arc((circle.center.x, circle.center.y), 2 * circle.radius, 2 * circle.radius, angle=0, theta1=0, theta2=360, color=edge_style.color, alpha=edge_style.alpha, linestyle=edge_style.linestyle, linewidth=edge_style.linewidth)) if edge_style.plot_points: ax.plot([circle.start.x], [circle.start.y], 'o', color=edge_style.color, alpha=edge_style.alpha) if edge_style.equal_aspect: ax.set_aspect('equal') return ax
[docs] def random_color(): """Random color generator.""" return random.random(), random.random(), random.random()
[docs] def split_wire_by_plane(wire, plane3d): """ Splits a wire into two parts using a plane. This method splits a wire into two parts based on the intersection points between the wire's primitives (edges) and a given 3D plane. It first finds the intersection points between each primitive and the plane, excluding duplicate points. Then, it checks if the number of intersection points is greater than one. If so, it raises a NotImplementedError, as the split is ambiguous. Otherwise, it performs the split using the `split_with_sorted_points` method of the wire object. The resulting wire objects are returned as a tuple. Note: The method assumes that the wire and the plane are in the same coordinate system. :param wire: The wire object to be split. :param plane3d: The 3D plane object used for splitting the wire. :return: A tuple containing two wire objects resulting from the split. :raises: NotImplementedError: If the wire intersects the plane at more than one point. :Example: >>> from design3d import Point3D, Vector3D >>> from design3d.surfaces import Plane3D >>> from design3d.core import EdgeStyle >>> from design3d.utils.common_operations import random_color >>> from design3d.models.open_rounded_line_segments import open_rounded_line_segements >>> plane = Plane3D.from_plane_vectors(Point3D(0.4, 0.4, 0.2), Vector3D(1, 0, 0), Vector3D(0, 1, 0)) >>> split_wire1,split_wire2 = split_wire_by_plane(open_rounded_line_segements, plane) >>> ax = open_rounded_line_segements.plot() >>> plane.plot(ax) >>> split_wire1.plot(ax, EdgeStyle(random_color())) >>> split_wire2.plot(ax, EdgeStyle(random_color())) """ wire_plane_intersections = [] for primitive in wire.primitives: intersections = plane3d.edge_intersections(primitive) for intersection in intersections: if not intersection.in_list(wire_plane_intersections): wire_plane_intersections.append(intersection) if len(wire_plane_intersections) > 1: raise NotImplementedError wire1, wire2 = wire.split_with_sorted_points([wire_plane_intersections[0], wire.primitives[-1].end]) return wire1, wire2
[docs] def plot_components_from_points(points, close_plot: bool = False): """ Gets Matplotlib components from points. :param points: given points. :param close_plot: Weather to close the plot or not. :return: """ components = [[], [], []] for point in points: for i, component in enumerate(point): components[i].append(component) valid_components = [] for list_components in components: if list_components: if close_plot: list_components.append(list_components[0]) valid_components.append(list_components) return valid_components
[docs] def plot_from_discretization_points(ax, edge_style, element, number_points: int = None, close_plot: bool = False): """ General plot method using discretization_points method to generate points. :param ax: Matplotlib plot. :param edge_style: edge_style to be applied to plot. :param element: design3d element to be plotted (either 2D or 3D). :param number_points: number of points to be used in the plot. :param close_plot: specifies if plot is to be closed or not. :return: Matplotlib plot axis. """ points = element.discretization_points(number_points=number_points) valid_components = plot_components_from_points(points, close_plot) ax.plot(*valid_components, color=edge_style.color, alpha=edge_style.alpha) return ax
[docs] def minimum_distance_points_circle3d_linesegment3d(circle3d, linesegment3d): """ Gets the points from the arc and the line that gives the minimal distance between them. :param circle3d: Circle 3d or Arc 3d. :param linesegment3d: Other line segment 3d. :return: Minimum distance points. """ def distance_squared(x, u_param, v_param, k_param, w_param): """Calculates the squared distance.""" return (u_param.dot(u_param) * x[0] ** 2 + w_param.dot(w_param) + v_param.dot(v_param) * ( (math.sin(x[1])) ** 2) * radius ** 2 + k_param.dot(k_param) * ((math.cos(x[1])) ** 2) * radius ** 2 - 2 * x[0] * w_param.dot(u_param) - 2 * x[0] * radius * math.sin(x[1]) * u_param.dot(v_param) - 2 * x[ 0] * radius * math.cos(x[1]) * u_param.dot(k_param) + 2 * radius * math.sin(x[1]) * w_param.dot(v_param) + 2 * radius * math.cos(x[1]) * w_param.dot(k_param) + math.sin(2 * x[1]) * v_param.dot(k_param) * radius ** 2) radius = circle3d.radius linseg_direction_vector = linesegment3d.direction_vector() vector_point_origin = circle3d.point_at_abscissa(0.0) - circle3d.frame.origin vector_point_origin = vector_point_origin.unit_vector() w = circle3d.frame.origin - linesegment3d.start v = circle3d.frame.w.cross(vector_point_origin) results = [] for initial_value in [np.array([0.5, circle3d.angle / 2]), np.array([0.5, 0]), np.array([0.5, circle3d.angle])]: results.append(least_squares(distance_squared, initial_value, bounds=[(0, 0), (1, circle3d.angle)], args=(linseg_direction_vector, v, vector_point_origin, w))) point1 = linesegment3d.point_at_abscissa(results[0].x[0] * linesegment3d.length()) point2 = circle3d.point_at_abscissa(results[1].x[1] * circle3d.radius) for couple in results[1:]: ptest1 = linesegment3d.point_at_abscissa(couple.x[0] * linesegment3d.length()) ptest2 = circle3d.point_at_abscissa(couple.x[1] * circle3d.radius) if ptest1.point_distance(ptest2) < v.dot(v): point1, point2 = ptest1, ptest2 return point1, point2
[docs] def get_abscissa_discretization(primitive, abscissa1, abscissa2, max_number_points: int = 10, return_abscissas: bool = True): """ Gets n discretization points between two given points of the edge. :param primitive: Primitive to discretize locally. :param abscissa1: Initial abscissa. :param abscissa2: Final abscissa. :param max_number_points: Expected number of points to discretize locally. :param return_abscissas: By default, returns also a list of abscissas corresponding to the discretization points :return: list of locally discretized point and a list containing the abscissas' values. """ discretized_points_between_1_2 = [] points_abscissas = [] for abscissa in np.linspace(abscissa1, abscissa2, num=max_number_points): if abscissa > primitive.length() + 1e-6: continue abscissa_point = primitive.point_at_abscissa(abscissa) if not abscissa_point.in_list(discretized_points_between_1_2): discretized_points_between_1_2.append(abscissa_point) points_abscissas.append(abscissa) if return_abscissas: return discretized_points_between_1_2, points_abscissas return discretized_points_between_1_2
[docs] def get_point_distance_to_edge(edge, point, start, end): """ Calculates the distance from a given point to an edge. :param edge: Edge to calculate distance to point. :param point: Point to calculate the distance to edge. :param start: Edge's start point. :param end: Edge's end point. :return: distance to edge. """ best_distance = math.inf if start != end: if start.is_close(end): if not edge.periodic: return point.point_distance(start) number_points = 10 if abs(0 - edge.length()) > 5e-6 else 2 else: number_points = 10 if abs(edge.abscissa(start) - edge.abscissa(end)) > 5e-6 else 2 # number_points = 10 if abs(0 - edge.length()) > 5e-6 else 2 elif edge.periodic: number_points = 10 if abs(0 - edge.length()) > 5e-6 else 2 distance = best_distance point1_ = start point2_ = end linesegment_class_ = getattr(design3d.edges, 'LineSegment' + edge.__class__.__name__[-2:]) while True: discretized_points_between_1_2 = edge.local_discretization(point1_, point2_, number_points) if not discretized_points_between_1_2: break distance = point.point_distance(discretized_points_between_1_2[0]) for point1, point2 in zip(discretized_points_between_1_2[:-1], discretized_points_between_1_2[1:]): if point1.is_close(point2): continue line = linesegment_class_(point1, point2) dist = line.point_distance(point) if dist < distance: point1_ = point1 point2_ = point2 distance = dist if not point1_ or math.isclose(distance, best_distance, abs_tol=1e-7): break best_distance = distance # if math.isclose(abscissa1, abscissa2, abs_tol=1e-6): # break return distance
[docs] def generic_minimum_distance(self, element, point1_edge1_, point2_edge1_, point1_edge2_, point2_edge2_, return_points=False): """ Gets the minimum distance between two elements. This is a generalized method in a case an analytical method has not yet been defined. :param element: other element. :param return_points: Weather to return the corresponding points or not. :return: distance to edge. """ n = max(1, int(self.length() / element.length())) best_distance = math.inf distance_points = None distance = best_distance linesegment_class_ = getattr(design3d.edges, 'LineSegment' + self.__class__.__name__[-2:]) while True: edge1_discretized_points_between_1_2 = self.local_discretization(point1_edge1_, point2_edge1_, number_points=10 * n) edge2_discretized_points_between_1_2 = element.local_discretization(point1_edge2_, point2_edge2_) if not edge1_discretized_points_between_1_2: break distance = edge2_discretized_points_between_1_2[0].point_distance(edge1_discretized_points_between_1_2[0]) distance_points = [edge2_discretized_points_between_1_2[0], edge1_discretized_points_between_1_2[0]] for point1_edge1, point2_edge1 in zip(edge1_discretized_points_between_1_2[:-1], edge1_discretized_points_between_1_2[1:]): lineseg1 = linesegment_class_(point1_edge1, point2_edge1) for point1_edge2, point2_edge2 in zip(edge2_discretized_points_between_1_2[:-1], edge2_discretized_points_between_1_2[1:]): lineseg2 = linesegment_class_(point1_edge2, point2_edge2) dist, min_dist_point1_, min_dist_point2_ = lineseg1.minimum_distance(lineseg2, True) if dist < distance: point1_edge1_, point2_edge1_ = point1_edge1, point2_edge1 point1_edge2_, point2_edge2_ = point1_edge2, point2_edge2 distance = dist distance_points = [min_dist_point1_, min_dist_point2_] if math.isclose(distance, best_distance, abs_tol=1e-6): break best_distance = distance n = 1 if return_points: return distance, distance_points[0], distance_points[1] return distance
[docs] def ellipse_abscissa_angle_integration(ellipse3d, point_abscissa, angle_start, initial_angle): """ Calculates the angle for a given abscissa point by integrating the ellipse. :param ellipse3d: the Ellipse3D. :param point_abscissa: the given abscissa for given point. :param angle_start: Ellipse3D / ArcEllipse3D start angle. (0 for Ellipse3D). :param initial_angle: angle abscissa's initial value. :return: final angle abscissa's value. """ def ellipse_arc_length(theta): return math.sqrt((ellipse3d.major_axis ** 2) * math.sin(theta) ** 2 + (ellipse3d.minor_axis ** 2) * math.cos(theta) ** 2) iter_counter = 0 while True: res, _ = scipy_integrate.quad(ellipse_arc_length, angle_start, initial_angle) if math.isclose(res, point_abscissa, abs_tol=1e-8): abscissa_angle = initial_angle break if res > point_abscissa: increment_factor = (abs(initial_angle - angle_start) * (point_abscissa - res)) / (2 * abs(res)) elif res == 0.0: increment_factor = 1e-5 else: increment_factor = (abs(initial_angle - angle_start) * (point_abscissa - res)) / abs(res) initial_angle += increment_factor iter_counter += 1 return abscissa_angle
[docs] def get_plane_equation_coefficients(plane_frame): """ Returns the a,b,c,d coefficient from equation ax+by+cz+d = 0. """ a, b, c = plane_frame.w d = -plane_frame.origin.dot(plane_frame.w) return round(a, 12), round(b, 12), round(c, 12), round(d, 12)
[docs] def get_plane_point_distance(plane_frame, point3d): """ Gets distance between a point and plane, using its frame. :param plane_frame: plane's frame. :param point3d: other point. :return: point plane distance. """ coefficient_a, coefficient_b, coefficient_c, coefficient_d = get_plane_equation_coefficients(plane_frame) return abs(plane_frame.w.dot(point3d) + coefficient_d) / math.sqrt(coefficient_a ** 2 + coefficient_b ** 2 + coefficient_c ** 2)
[docs] def order_points_list_for_nearest_neighbor(points): """ Given a list of unordered points defining a path, it will order these points considering the nearest neighbor. """ ordered_points = [] remaining_points = np.array([[*point] for point in points]) current_point = remaining_points[0, :] remaining_points = np.delete(remaining_points, 0, axis=0) while remaining_points.any(): nearest_point_idx = np.argmin(np.linalg.norm(remaining_points - current_point, axis=1)) nearest_point = remaining_points[nearest_point_idx, :] remaining_points = np.delete(remaining_points, nearest_point_idx, axis=0) ordered_points.append(design3d.Point3D(*current_point)) current_point = nearest_point # Add the last point to complete the loop ordered_points.append(design3d.Point3D(*current_point)) return ordered_points
[docs] def separate_points_by_closeness(points): """ Separates a list of 3D Cartesian points into two groups based on their spatial closeness using DBSCAN. This function applies the DBSCAN (Density-Based Spatial Clustering of Applications with Noise) algorithm to the given list of 3D Cartesian points. DBSCAN clusters the points based on their spatial proximity. The points are separated into two groups, 'group1' and 'group2', depending on their spatial closeness as determined by the DBSCAN clustering. Please note that the 'eps' parameter inside the function can be adjusted to control the closeness threshold. :param points: A list of 3D Cartesian points, where each point is represented as a list of three coordinates. :return: - group1 (list of lists): The first group of points based on their closeness. - group2 (list of lists): The second group of points based on their closeness. """ if not points: return [] points_ = np.array([[*point] for point in points]) # Apply DBSCAN clustering with a small epsilon to separate close points distances = sorted(np.linalg.norm(points_[1:] - points_[0], axis=1)) eps = max(min(np.mean(distances[:max(int(len(points)*0.1), 30)]) / 2, 0.35), 0.02) # eps = np.mean(distances[:max(int(len(points)*0.1), 30)]) / 2 dbscan = DBSCAN(eps=eps, min_samples=1) labels = dbscan.fit_predict(points_) # Initialize two empty lists for the two groups groups = {} # Assign points to group1 or group2 based on DBSCAN labels for i, label in enumerate(labels): if label not in groups: groups[label] = [points[i]] continue groups[label].append(points[i]) keys = list(groups.keys()) for key in keys: groups[key] = order_points_list_for_nearest_neighbor(groups[key]) groups[key].append(groups[key][0]) return list(groups.values())
[docs] def get_center_of_mass(list_points): """ Gets the center of mass of a given list of points. :param list_points: list of points to get the center of mass from. :return: center of mass point. """ center_mass = design3d.O3D for point in list_points: center_mass += point center_mass /= len(list_points) return center_mass