Source code for design3d.surfaces

"""design3d module for 3D Surfaces."""
import math
import traceback
import warnings
from collections import deque
from functools import cached_property, lru_cache
from itertools import chain
from typing import List, Union

import numpy as np
from numpy.typing import NDArray
import matplotlib.pyplot as plt
import triangle as triangle_lib

from geomdl import NURBS, BSpline
from scipy.linalg import lu_factor, lu_solve
from scipy.optimize import least_squares, minimize

import design3d.nurbs.helpers as nurbs_helpers
from design3d.nurbs.helpers import generate_knot_vector
import design3d.core
from design3d.base import DataEqualityObject
import design3d.geometry
import design3d.utils.common_operations as d3d_common_operations
import design3d.utils.intersections as d3d_utils_intersections
import design3d.utils.parametric as d3d_parametric
from design3d import display, edges, grid, wires, curves
from design3d.core import EdgeStyle
from design3d.nurbs.core import evaluate_surface, derivatives_surface, point_inversion
from design3d.nurbs.fitting import approximate_surface, interpolate_surface
from design3d.nurbs.operations import (split_surface_u, split_surface_v, decompose_surface,
                                      extract_surface_curve_u, extract_surface_curve_v)
from design3d.utils.parametric import (array_range_search, repair_start_end_angle_periodicity, angle_discontinuity,
                                      find_parametric_point_at_singularity, is_isocurve,
                                      verify_repeated_parametric_points, repair_undefined_brep)


[docs] def knots_vector_inv(knots_vector): """ Compute knot-elements and multiplicities based on the global knot vector. """ knots = sorted(set(knots_vector)) multiplicities = [knots_vector.count(knot) for knot in knots] return knots, multiplicities
[docs] class Surface2D(DataEqualityObject): """ A surface bounded by an outer contour. """ def __init__(self, outer_contour: wires.Contour2D, inner_contours: List[wires.Contour2D], name: str = 'name'): self.outer_contour = outer_contour self.inner_contours = inner_contours self._area = None self.name=name def __hash__(self): """ Calculate the hash value for Surface2D. This method is used to generate a hash value for instances of the current class, which can be used for hash-based data structures like dictionaries and sets. The hash value is computed based on the combined hash of the outer contour and a tuple of hash values for the inner contours. This ensures that objects with equivalent contours will have the same hash value, allowing them to be efficiently compared and retrieved from hash-based collections. :return: A hash value representing the object's state. :rtype: int """ return hash((self.outer_contour, tuple(self.inner_contours))) def _data_hash(self): return hash(self) def __eq__(self, other): return self.outer_contour == other.outer_contour and self.inner_contours == other.inner_contours
[docs] def copy(self, deep=True, memo=None): """ Copies the surface2d. """ return self.__class__(outer_contour=self.outer_contour.copy(deep=deep, memo=memo), inner_contours=[c.copy(deep, memo) for c in self.inner_contours], name='copy_' + self.name)
[docs] def area(self): """ Computes the area of the surface. """ if not self._area: self._area = self.outer_contour.area() - sum(contour.area() for contour in self.inner_contours) return self._area
[docs] def second_moment_area(self, point: design3d.Point2D): """ Computes the second moment area of the surface. """ i_x, i_y, i_xy = self.outer_contour.second_moment_area(point) for contour in self.inner_contours: i_xc, i_yc, i_xyc = contour.second_moment_area(point) i_x -= i_xc i_y -= i_yc i_xy -= i_xyc return i_x, i_y, i_xy
[docs] def center_of_mass(self): """ Compute the center of mass of the 2D surface. :return: The center of mass of the surface. :rtype: :class:`design3d.Point2D` """ center = self.outer_contour.area() * self.outer_contour.center_of_mass() for contour in self.inner_contours: center -= contour.area() * contour.center_of_mass() return center / self.area()
[docs] def point_belongs(self, point2d: design3d.Point2D, include_edge_points: bool = True): """ Check whether a point belongs to the 2D surface. :param point2d: The point to check. :type point2d: :class:`design3d.Point2D` :return: True if the point belongs to the surface, False otherwise. :rtype: bool """ if not self.outer_contour.point_inside(point2d, include_edge_points=include_edge_points): return False for inner_contour in self.inner_contours: if inner_contour.point_inside(point2d, include_edge_points=False): return False return True
[docs] def random_point_inside(self): """ Generate a random point inside the 2D surface. Taking into account any inner contours (holes) it may have. :return: A random point inside the surface. :rtype: :class:`design3d.Point2D` """ point_inside_outer_contour = None center_of_mass = self.center_of_mass() if self.point_belongs(center_of_mass, False): point_inside_outer_contour = center_of_mass if not point_inside_outer_contour: point_inside_outer_contour = self.outer_contour.random_point_inside() while True: inside_inner_contour = False for inner_contour in self.inner_contours: if inner_contour.point_inside(point_inside_outer_contour): inside_inner_contour = True if not inside_inner_contour and \ point_inside_outer_contour is not None: break point_inside_outer_contour = self.outer_contour.random_point_inside() return point_inside_outer_contour
[docs] @staticmethod def 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)), } triagulation = triangle_lib.triangulate(tri, tri_opt) return display.Mesh2D(vertices=triagulation['vertices'], triangles=triagulation['triangles'])
[docs] def triangulation(self, number_points_x: int = 15, number_points_y: int = 15): """ Triangulates the Surface2D using the Triangle library. :param number_points_x: Number of discretization points in x direction. :type number_points_x: int :param number_points_y: Number of discretization points in y direction. :type number_points_y: int :return: The triangulated surface as a display mesh. :rtype: :class:`design3d.display.Mesh2D` """ area = self.bounding_rectangle().area() tri_opt = "p" if math.isclose(area, 0., abs_tol=1e-12): return None triangulates_with_grid = number_points_x > 0 and number_points_y > 0 discretize_line = number_points_x > 0 or number_points_y > 0 if not triangulates_with_grid: tri_opt = "p" discretize_line_direction = "xy" if number_points_y == 0 or number_points_x > 25 * number_points_y: discretize_line_direction = "x" elif number_points_y > 20 * number_points_x: discretize_line_direction = "y" outer_polygon = self.outer_contour.to_polygon(angle_resolution=15, discretize_line=discretize_line, discretize_line_direction=discretize_line_direction) if not self.inner_contours and not triangulates_with_grid: return outer_polygon.triangulation() points_grid, x, y, grid_point_index = outer_polygon.grid_triangulation_points(number_points_x=number_points_x, number_points_y=number_points_y) points = outer_polygon.points.copy() points_set = set(points) if len(points_set) < 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 self.inner_contours: # No holes return self.triangulation_without_holes(vertices, segments, points_grid, tri_opt) point_index = {p: i for i, p in enumerate(points)} holes = [] for inner_contour in self.inner_contours: inner_polygon = inner_contour.to_polygon(angle_resolution=10, discretize_line=discretize_line, discretize_line_direction=discretize_line_direction) inner_polygon_nodes = inner_polygon.points.copy() 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 triangulates_with_grid: # removes with a region search the grid points that are in the inner contour xmin, xmax, ymin, ymax = inner_polygon.bounding_rectangle.bounds() x_grid_range = array_range_search(x, xmin, xmax) y_grid_range = array_range_search(y, ymin, ymax) for i in x_grid_range: for j in y_grid_range: point = grid_point_index.get((i, j)) if not point: continue if inner_polygon.point_inside(point): points_grid.remove(point) grid_point_index.pop((i, j)) if triangulates_with_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 display.Mesh2D(vertices=triangulation['vertices'], triangles=triangulation['triangles'])
[docs] def split_by_lines(self, lines): """ Returns a list of cut surfaces given by the lines provided as argument. """ cutted_surfaces = [] iteration_surfaces = self.cut_by_line(lines[0]) for line in lines[1:]: iteration_surfaces2 = [] for surface in iteration_surfaces: line_cutted_surfaces = surface.cut_by_line(line) llcs = len(line_cutted_surfaces) if llcs == 1: cutted_surfaces.append(line_cutted_surfaces[0]) else: iteration_surfaces2.extend(line_cutted_surfaces) iteration_surfaces = iteration_surfaces2[:] cutted_surfaces.extend(iteration_surfaces) return cutted_surfaces
[docs] def split_regularly(self, n): """ Split in n slices. """ bounding_rectangle = self.outer_contour.bounding_rectangle lines = [] for i in range(n - 1): xi = bounding_rectangle[0] + (i + 1) * (bounding_rectangle[1] - bounding_rectangle[0]) / n lines.append(curves.Line2D(design3d.Point2D(xi, 0), design3d.Point2D(xi, 1))) return self.split_by_lines(lines)
[docs] def cut_by_line(self, line: curves.Line2D): """ Returns a list of cut Surface2D by the given line. :param line: The line to cut the Surface2D with. :type line: :class:`curves.Line2D` :return: A list of 2D surfaces resulting from the cut. :rtype: List[:class:`design3d.faces.Surface2D`] """ surfaces = [] splitted_outer_contours = self.outer_contour.cut_by_line(line) splitted_inner_contours_table = [] for inner_contour in self.inner_contours: splitted_inner_contours = inner_contour.cut_by_line(line) splitted_inner_contours_table.append(splitted_inner_contours) # First part of the external contour for outer_split in splitted_outer_contours: inner_contours = [] for splitted_inner_contours in splitted_inner_contours_table: for inner_split in splitted_inner_contours: inner_split.order_contour() point = inner_split.random_point_inside() if outer_split.point_inside(point): inner_contours.append(inner_split) if inner_contours: surface2d = self.from_contours(outer_split, inner_contours) surfaces.append(surface2d) else: surfaces.append(Surface2D(outer_split, [])) return surfaces
[docs] def line_crossings(self, line: curves.Line2D): """ Find intersection points between a line and the 2D surface. :param line: The line to intersect with the shape. :type line: :class:`curves.Line2D` :return: A list of intersection points sorted by increasing abscissa along the line. Each intersection point is a tuple (point, primitive) where point is the intersection point and primitive is the intersected primitive. :rtype: List[Tuple[:class:`design3d.Point2D`, :class:`design3d.core.Primitive2D`]] """ intersection_points = [] for primitive in self.outer_contour.primitives: for point in primitive.line_crossings(line): if (point, primitive) not in intersection_points: intersection_points.append((point, primitive)) for inner_contour in self.inner_contours: for primitive in inner_contour.primitives: for point in primitive.line_crossings(line): if (point, primitive) not in intersection_points: intersection_points.append((point, primitive)) return sorted(intersection_points, key=lambda ip: line.abscissa(ip[0]))
[docs] def split_at_centers(self): """ Split in n slices. # TODO: is this used ? """ cutted_contours = [] center_of_mass1 = self.inner_contours[0].center_of_mass() center_of_mass2 = self.inner_contours[1].center_of_mass() cut_line = curves.Line2D(center_of_mass1, center_of_mass2) iteration_contours2 = [] surface_cut = self.cut_by_line(cut_line) iteration_contours2.extend(surface_cut) iteration_contours = iteration_contours2[:] cutted_contours.extend(iteration_contours) return cutted_contours
[docs] def bounding_rectangle(self): """ Returns bounding rectangle. :return: Returns a python object with useful methods :rtype: :class:`design3d.core.BoundingRectangle """ return self.outer_contour.bounding_rectangle
[docs] @classmethod def from_contours(cls, outer_contour, inner_contours): """ Create a Surface2D object from an outer contour and a list of inner contours. :param outer_contour: The outer contour that bounds the surface. :type outer_contour: wires.Contour2D :param inner_contours: The list of inner contours that define the holes of the surface. :type inner_contours : List[wires.Contour2D] :return: Surface2D defined by the given contours. """ surface2d_inner_contours = [] surface2d_outer_contour = outer_contour for inner_contour in inner_contours: if surface2d_outer_contour.shared_primitives_extremities( inner_contour): # inner_contour will be merged with outer_contour merged_contours = surface2d_outer_contour.merge_with( inner_contour) if len(merged_contours) >= 2: raise NotImplementedError surface2d_outer_contour = merged_contours[0] else: # inner_contour will be added to the inner contours of the # Surface2D surface2d_inner_contours.append(inner_contour) return cls(surface2d_outer_contour, surface2d_inner_contours)
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5, equal_aspect=False), **kwargs): """Plot surface 2d using Matplotlib.""" if ax is None: _, ax = plt.subplots() self.outer_contour.plot(ax=ax, edge_style=edge_style) for inner_contour in self.inner_contours: inner_contour.plot(ax=ax, edge_style=edge_style) if edge_style.equal_aspect: ax.set_aspect('equal') ax.margins(0.1) return ax
[docs] def axial_symmetry(self, line): """ Finds out the symmetric 2D surface according to a line. """ outer_contour = self.outer_contour.axial_symmetry(line) inner_contours = [] if self.inner_contours: inner_contours = [contour.axial_symmetry(line) for contour in self.inner_contours] return self.__class__(outer_contour=outer_contour, inner_contours=inner_contours)
[docs] def rotation(self, center, angle): """ Surface2D rotation. :param center: rotation center. :param angle: angle rotation. :return: a new rotated Surface2D. """ outer_contour = self.outer_contour.rotation(center, angle) if self.inner_contours: inner_contours = [contour.rotation(center, angle) for contour in self.inner_contours] else: inner_contours = [] return self.__class__(outer_contour, inner_contours)
[docs] def translation(self, offset: design3d.Vector2D): """ Surface2D translation. :param offset: translation vector. :return: A new translated Surface2D. """ outer_contour = self.outer_contour.translation(offset) inner_contours = [contour.translation(offset) for contour in self.inner_contours] return self.__class__(outer_contour, inner_contours)
[docs] def frame_mapping(self, frame: design3d.Frame2D, side: str): """Frame mapping of a surface 2d.""" outer_contour = self.outer_contour.frame_mapping(frame, side) inner_contours = [contour.frame_mapping(frame, side) for contour in self.inner_contours] return self.__class__(outer_contour, inner_contours)
[docs] def geo_lines(self): # , mesh_size_list=None): """ Gets the lines that define a Surface2D in a .geo file. """ i, i_p = None, None lines, line_surface, lines_tags = [], [], [] point_account, line_account, line_loop_account = 0, 0, 1 for outer_contour, contour in enumerate(list(chain(*[[self.outer_contour], self.inner_contours]))): if isinstance(contour, curves.Circle2D): points = [design3d.Point2D(contour.center.x - contour.radius, contour.center.y), contour.center, design3d.Point2D(contour.center.x + contour.radius, contour.center.y)] index = [] for i, point in enumerate(points): lines.append(point.get_geo_lines(tag=point_account + i + 1, point_mesh_size=None)) index.append(point_account + i + 1) lines.append('Circle(' + str(line_account + 1) + ') = {' + str(index[0]) + ', ' + str(index[1]) + ', ' + str(index[2]) + '};') lines.append('Circle(' + str(line_account + 2) + ') = {' + str(index[2]) + ', ' + str(index[1]) + ', ' + str(index[0]) + '};') lines_tags.append(line_account + 1) lines_tags.append(line_account + 2) lines.append('Line Loop(' + str(outer_contour + 1) + ') = {' + str(lines_tags)[1:-1] + '};') line_surface.append(line_loop_account) point_account = point_account + 2 + 1 line_account, line_loop_account = line_account + 1 + 1, line_loop_account + 1 lines_tags = [] elif isinstance(contour, (wires.Contour2D, wires.ClosedPolygon2D)): if not isinstance(contour, wires.ClosedPolygon2D): contour = contour.to_polygon(1) for i, point in enumerate(contour.points): lines.append(point.get_geo_lines(tag=point_account + i + 1, point_mesh_size=None)) for i_p, primitive in enumerate(contour.primitives): if i_p != len(contour.primitives) - 1: lines.append(primitive.get_geo_lines(tag=line_account + i_p + 1, start_point_tag=point_account + i_p + 1, end_point_tag=point_account + i_p + 2)) else: lines.append(primitive.get_geo_lines(tag=line_account + i_p + 1, start_point_tag=point_account + i_p + 1, end_point_tag=point_account + 1)) lines_tags.append(line_account + i_p + 1) lines.append('Line Loop(' + str(outer_contour + 1) + ') = {' + str(lines_tags)[1:-1] + '};') line_surface.append(line_loop_account) point_account = point_account + i + 1 line_account, line_loop_account = line_account + i_p + 1, line_loop_account + 1 lines_tags = [] lines.append('Plane Surface(' + str(1) + ') = {' + str(line_surface)[1:-1] + '};') return lines
[docs] def mesh_lines(self, factor: float, curvature_mesh_size: int = None, min_points: int = None, initial_mesh_size: float = 5): """ Gets the lines that define mesh parameters for a Surface2D, to be added to a .geo file. :param factor: A float, between 0 and 1, that describes the mesh quality (1 for coarse mesh - 0 for fine mesh) :type factor: float :param curvature_mesh_size: Activate the calculation of mesh element sizes based on curvature (with curvature_mesh_size elements per 2*Pi radians), defaults to 0 :type curvature_mesh_size: int, optional :param min_points: Check if there are enough points on small edges (if it is not, we force to have min_points on that edge), defaults to None :type min_points: int, optional :param initial_mesh_size: If factor=1, it will be initial_mesh_size elements per dimension, defaults to 5 :type initial_mesh_size: float, optional :return: A list of lines that describe mesh parameters :rtype: List[str] """ lines = [] if factor == 0: factor = 1e-3 size = (math.sqrt(self.area()) / initial_mesh_size) * factor if min_points: lines.extend(self.get_mesh_lines_with_transfinite_curves( [[self.outer_contour], self.inner_contours], min_points, size)) lines.append('Field[1] = MathEval;') lines.append('Field[1].F = "' + str(size) + '";') lines.append('Background Field = 1;') if curvature_mesh_size: lines.append('Mesh.MeshSizeFromCurvature = ' + str(curvature_mesh_size) + ';') return lines
[docs] @staticmethod def get_mesh_lines_with_transfinite_curves(lists_contours, min_points, size): """Gets Surface 2d mesh lines with transfinite curves.""" lines, primitives, primitives_length = [], [], [] circle_class_ = getattr(wires, 'Circle' + lists_contours[0][0].__class__.__name__[-2:]) for contour in list(chain(*lists_contours)): if isinstance(contour, circle_class_): primitives.append(contour) primitives.append(contour) primitives_length.append(contour.length() / 2) primitives_length.append(contour.length() / 2) else: for primitive in contour.primitives: if (primitive not in primitives) and (primitive.reverse() not in primitives): primitives.append(primitive) primitives_length.append(primitive.length()) for i, length in enumerate(primitives_length): if length < min_points * size: lines.append('Transfinite Curve {' + str(i) + '} = ' + str(min_points) + ' Using Progression 1;') return lines
[docs] def to_geo(self, file_name: str, factor: float, **kwargs): # curvature_mesh_size: int = None, # min_points: int = None, # initial_mesh_size: float = 5): """ Gets the .geo file for the Surface2D. """ for element in [('curvature_mesh_size', 0), ('min_points', None), ('initial_mesh_size', 5)]: if element[0] not in kwargs: kwargs[element[0]] = element[1] lines = self.geo_lines() lines.extend(self.mesh_lines(factor, kwargs['curvature_mesh_size'], kwargs['min_points'], kwargs['initial_mesh_size'])) 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 to_msh(self, file_name: str, mesh_dimension: int, mesh_order: int, factor: float, **kwargs): # curvature_mesh_size: int = 0, # min_points: int = None, # initial_mesh_size: float = 5): """ Gets .msh file for the Surface2D generated by gmsh. :param file_name: The msh. file name :type file_name: str :param mesh_dimension: The mesh dimension (1: 1D-Edge, 2: 2D-Triangle, 3D-Tetrahedra) :type mesh_dimension: int :param factor: A float, between 0 and 1, that describes the mesh quality (1 for coarse mesh - 0 for fine mesh) :type factor: float :param curvature_mesh_size: Activate the calculation of mesh element sizes based on curvature (with curvature_mesh_size elements per 2*Pi radians), defaults to 0 :type curvature_mesh_size: int, optional :param min_points: Check if there are enough points on small edges (if it is not, we force to have min_points on that edge), defaults to None :type min_points: int, optional :param initial_mesh_size: If factor=1, it will be initial_mesh_size elements per dimension, defaults to 5 :type initial_mesh_size: float, optional :return: A txt file :rtype: .txt """ for element in [('curvature_mesh_size', 0), ('min_points', None), ('initial_mesh_size', 5)]: if element[0] not in kwargs: kwargs[element[0]] = element[1] self.to_geo(file_name=file_name, mesh_dimension=mesh_dimension, factor=factor, curvature_mesh_size=kwargs['curvature_mesh_size'], min_points=kwargs['min_points'], initial_mesh_size=kwargs['initial_mesh_size']) design3d.core.VolumeModel.generate_msh_file(file_name, mesh_dimension, mesh_order)
[docs] class Surface3D(DataEqualityObject): """ Abstract class. """ x_periodicity = None y_periodicity = None face_class = None def __init__(self, frame: design3d.Frame3D = None, name: str = ''): self.frame = frame self.name=name @property def u_domain(self): """The parametric domain of the surface in the U direction.""" return -math.inf, math.inf @property def v_domain(self): """The parametric domain of the surface in the V direction.""" return -math.inf, math.inf @property def domain(self): """Returns u and v bounds.""" umin, umax = self.u_domain d3din, d3dax = self.v_domain return umin, umax, d3din, d3dax
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), **kwargs): """ Abstract method. """ raise NotImplementedError(f"plot method is not implemented for {self.__class__.__name__}")
[docs] def point2d_to_3d(self, point2d): """ Abstract method. """ raise NotImplementedError(f'point2d_to_3d is abstract and should be implemented in {self.__class__.__name__}')
[docs] def point3d_to_2d(self, point3d): """ Abstract method. Convert a 3D point to a 2D parametric point. :param point3d: The 3D point to convert, represented by 3 coordinates (x, y, z). :type point3d: `design3d.Point3D` :return: NotImplementedError: If the method is not implemented in the subclass. """ raise NotImplementedError(f'point3d_to_2d is abstract and should be implemented in {self.__class__.__name__}')
[docs] def face_from_contours3d(self, contours3d: List[wires.Contour3D], name: str = ''): """Deprecated method, 'Use Face3D from_contours3d method'.""" raise AttributeError('Use Face3D from_contours3d method')
[docs] def repair_primitives_periodicity(self, primitives2d, primitives_mapping): """ Repairs the continuity of the 2D contour while using contour3d_to_2d on periodic surfaces. :param primitives2d: The primitives in parametric surface domain. :type primitives2d: list :param primitives_mapping: It is a dictionary that stores the correspondence between primitives in the parametric domain with their equivalent primitive in 3D space. :type primitives_mapping: dict :return: A list of primitives. :rtype: list """ # pylint: disable= too-many-locals tol = 1e-2 if self.__class__.__name__ == "ExtrusionSurface3D": tol = 5e-6 x_periodicity = self.x_periodicity if self.x_periodicity else -1 y_periodicity = self.y_periodicity if self.y_periodicity else -1 if x_periodicity or y_periodicity: if self.is_undefined_brep(primitives2d[0]): old_primitive = primitives2d[0] primitives2d[0] = self.fix_undefined_brep_with_neighbors(primitives2d[0], primitives2d[-1], primitives2d[1]) primitives_mapping[primitives2d[0]] = primitives_mapping.pop(old_primitive) self._helper_repair_primitives_periodicity(primitives2d, primitives_mapping, [x_periodicity, y_periodicity], tol) if self.__class__.__name__ in ("SphericalSurface3D", "ConicalSurface3D", "RevolutionSurface3D"): delta = primitives2d[-1].end - primitives2d[0].start if (math.isclose(abs(delta.x), x_periodicity, abs_tol=tol) or math.isclose(abs(delta.y), y_periodicity, abs_tol=tol)): last_end_3d = self.point2d_to_3d(primitives2d[-1].end) first_start_3d = self.point2d_to_3d(primitives2d[0].start) if last_end_3d.is_close(first_start_3d, 1e-6) and not self.is_singularity_point(last_end_3d): old_primitive = primitives2d[0] primitives2d[0] = primitives2d[0].translation(delta) primitives_mapping[primitives2d[0]] = primitives_mapping.pop(old_primitive) self._helper_repair_primitives_periodicity(primitives2d, primitives_mapping, [x_periodicity, y_periodicity], tol) self.check_parametric_contour_end(primitives2d, tol)
def _helper_repair_primitives_periodicity(self, primitives2d, primitives_mapping, periodicities, tol): """Helper function to repair_primitives_periodicity.""" x_periodicity, y_periodicity = periodicities i = 1 while i < len(primitives2d): delta = primitives2d[i - 1].end - primitives2d[i].start distance = delta.norm() if not math.isclose(distance, 0, abs_tol=tol): if math.isclose(primitives2d[i].length(), x_periodicity, abs_tol=tol) or \ math.isclose(primitives2d[i].length(), y_periodicity, abs_tol=tol): delta_end = primitives2d[i - 1].end - primitives2d[i].end delta_min_index, _ = min(enumerate([distance, delta_end.norm()]), key=lambda x: x[1]) if self.is_undefined_brep(primitives2d[i]): repair_undefined_brep(self, primitives2d, primitives_mapping, i, primitives2d[i - 1]) elif self.is_singularity_point(self.point2d_to_3d(primitives2d[i - 1].end)) and \ self.is_singularity_point(self.point2d_to_3d(primitives2d[i].start)): self.repair_singularity(primitives2d, i, primitives2d[i - 1]) elif primitives2d[i].end.is_close(primitives2d[i - 1].end, tol=tol): self.repair_reverse(primitives2d, primitives_mapping, i) elif delta_min_index == 0: self.repair_translation(primitives2d, primitives_mapping, i, delta) else: old_primitive = primitives2d[i] new_primitive = primitives2d[i].reverse() primitives2d[i] = new_primitive.translation(delta_end) primitives_mapping[primitives2d[i]] = primitives_mapping.pop(old_primitive) elif primitives2d[i].end.is_close(primitives2d[i - 1].end, tol=tol): self.repair_reverse(primitives2d, primitives_mapping, i) elif self.is_undefined_brep(primitives2d[i]): repair_undefined_brep(self, primitives2d, primitives_mapping, i, primitives2d[i - 1]) elif self.is_singularity_point(self.point2d_to_3d(primitives2d[i - 1].end), tol=1e-5) and \ self.is_singularity_point(self.point2d_to_3d(primitives2d[i].start), tol=1e-5): self.repair_singularity(primitives2d, i, primitives2d[i - 1]) else: self.repair_translation(primitives2d, primitives_mapping, i, delta) i += 1
[docs] def check_parametric_contour_end(self, primitives2d, tol): """Helper function to repair_primitives_periodicity.""" previous_primitive = primitives2d[-1] delta = previous_primitive.end - primitives2d[0].start distance = delta.norm() is_connected = math.isclose(distance, 0, abs_tol=tol) if not is_connected and self.is_singularity_point(self.point2d_to_3d(previous_primitive.end)) and \ self.is_singularity_point(self.point2d_to_3d(primitives2d[0].start)): primitives2d.append(edges.LineSegment2D(previous_primitive.end, primitives2d[0].start, name="construction"))
[docs] @staticmethod def repair_singularity(primitives2d, i, previous_primitive): """Helper function to repair_primitives_periodicity.""" primitives2d.insert(i, edges.LineSegment2D(previous_primitive.end, primitives2d[i].start, name="construction")) if i < len(primitives2d): i += 1
[docs] @staticmethod def repair_reverse(primitives2d, primitives_mapping, i): """Helper function to repair_primitives_periodicity.""" old_primitive = primitives2d[i] primitives2d[i] = primitives2d[i].reverse() primitives_mapping[primitives2d[i]] = primitives_mapping.pop(old_primitive)
[docs] @staticmethod def repair_translation(primitives2d, primitives_mapping, i, delta): """Helper function to repair_primitives_periodicity.""" old_primitive = primitives2d[i] primitives2d[i] = primitives2d[i].translation(delta) primitives_mapping[primitives2d[i]] = primitives_mapping.pop(old_primitive)
[docs] def connect_contours(self, outer_contour, inner_contours): """ Abstract method. Repair 2D contours of a face on the parametric domain. :param outer_contour: Outer contour 2D. :type inner_contours: wires.Contour2D :param inner_contours: List of 2D contours. :type inner_contours: list :return: NotImplementedError: If the method is not implemented in the subclass. """ raise NotImplementedError(f'connect_contours is abstract and should be implemented in ' f'{self.__class__.__name__}')
[docs] @staticmethod def update_primitives_mapping(primitives_mapping, primitives, primitive3d): """ Helper function to contour3d_to_2d. """ for primitive in primitives: primitives_mapping[primitive] = primitive3d
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ points3d = [self.point2d_to_3d(design3d.Point2D(*point)) for point in points] return np.array(points3d)
[docs] def primitives3d_to_2d(self, primitives3d): """ Helper function to perform conversion of 3D primitives into B-Rep primitives. :param primitives3d: List of 3D primitives from a 3D contour. :type primitives3d: List[edges.Edge] :return: A list of 2D primitives on parametric domain. :rtype: List[edges.Edge] """ primitives2d = [] primitives_mapping = {} for primitive3d in primitives3d: method_name = f'{primitive3d.__class__.__name__.lower()}_to_2d' if hasattr(self, method_name): primitives = getattr(self, method_name)(primitive3d) if primitives is None: continue self.update_primitives_mapping(primitives_mapping, primitives, primitive3d) primitives2d.extend(primitives) else: raise AttributeError(f'Class {self.__class__.__name__} does not implement {method_name}') return primitives2d, primitives_mapping
[docs] def contour3d_to_2d(self, contour3d, return_primitives_mapping: bool = False): """ Transforms a Contour3D into a Contour2D in the parametric domain of the surface. :param contour3d: The contour to be transformed. :type contour3d: :class:`wires.Contour3D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 2D contour object. :rtype: :class:`wires.Contour2D` """ primitives2d, primitives_mapping = self.primitives3d_to_2d(contour3d.primitives) wire2d = wires.Wire2D(primitives2d) is_wire = False if self.x_periodicity and not self.is_singularity_point(self.point2d_to_3d(wire2d.primitives[0].start)) and \ not self.is_singularity_point(self.point2d_to_3d(wire2d.primitives[-1].end)): delta_x = abs(wire2d.primitives[0].start.x - wire2d.primitives[-1].end.x) if math.isclose(delta_x, self.x_periodicity, rel_tol=0.01) and wire2d.is_ordered(1e-3): is_wire = True if self.y_periodicity and not self.is_singularity_point(self.point2d_to_3d(wire2d.primitives[0].start)) and \ not self.is_singularity_point(self.point2d_to_3d(wire2d.primitives[-1].end)): delta_y = abs(wire2d.primitives[0].start.y - wire2d.primitives[-1].end.y) if math.isclose(delta_y, self.y_periodicity, rel_tol=0.01) and wire2d.is_ordered(1e-3): is_wire = True # Fix contour if not is_wire and (self.x_periodicity or self.y_periodicity): self.repair_primitives_periodicity(primitives2d, primitives_mapping) if return_primitives_mapping: return wires.Contour2D(primitives2d), primitives_mapping return wires.Contour2D(primitives2d)
[docs] def contour2d_to_3d(self, contour2d, return_primitives_mapping: bool = False): """ Transforms a Contour2D in the parametric domain of the surface into a Contour3D in Cartesian coordinate. :param contour2d: The contour to be transformed. :type contour2d: :class:`wires.Contour2D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 3D contour object. :rtype: :class:`wires.Contour3D` """ primitives3d = [] primitives_mapping = {} for primitive2d in contour2d.primitives: if self.is_degenerated_brep(primitive2d): continue method_name = f'{primitive2d.__class__.__name__.lower()}_to_3d' if hasattr(self, method_name): try: primitives = getattr(self, method_name)(primitive2d) if primitives is None: continue primitives3d.extend(primitives) primitives_mapping[primitive2d] = primitives[0] except AttributeError: print(traceback.format_exc()) print(f'Class {self.__class__.__name__} does not implement {method_name}' f'with {primitive2d.__class__.__name__}') else: raise AttributeError( f'Class {self.__class__.__name__} does not implement {method_name}') if not primitives3d: raise ValueError("no primitives to create contour") if return_primitives_mapping: return wires.Contour3D(primitives3d), primitives_mapping return wires.Contour3D(primitives3d)
[docs] def linesegment3d_to_2d(self, linesegment3d): """ A line segment on a surface will be in any case a line in 2D?. """ return [edges.LineSegment2D(self.point3d_to_2d(linesegment3d.start), self.point3d_to_2d(linesegment3d.end))]
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Is this right?. """ n = bspline_curve3d.ctrlpts.shape[0] points = [self.point3d_to_2d(p) for p in bspline_curve3d.discretization_points(number_points=n)] return [edges.BSplineCurve2D.from_points_interpolation(points, bspline_curve3d.degree)]
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Is this right?. """ n = len(bspline_curve2d.control_points) points = [self.point2d_to_3d(p) for p in bspline_curve2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, bspline_curve2d.degree, centripetal=True)]
[docs] def normal_from_point2d(self, point2d): """ Evaluates the normal vector of the bspline surface at this 2D point. """ raise NotImplementedError('NotImplemented')
[docs] def normal_from_point3d(self, point3d): """ Evaluates the normal vector of the bspline surface at this 3D point. """ return (self.normal_from_point2d(self.point3d_to_2d(point3d)))[1]
[docs] def geodesic_distance_from_points2d(self, point1_2d: design3d.Point2D, point2_2d: design3d.Point2D, number_points: int = 50): """ Approximation of geodesic distance via line segments length sum in 3D. """ # points = [point1_2d] current_point3d = self.point2d_to_3d(point1_2d) distance = 0. for i in range(number_points): next_point3d = self.point2d_to_3d(point1_2d + (i + 1) / number_points * (point2_2d - point1_2d)) distance += next_point3d.point_distance(current_point3d) current_point3d = next_point3d return distance
[docs] def geodesic_distance(self, point1_3d: design3d.Point3D, point2_3d: design3d.Point3D): """ Approximation of geodesic distance between 2 3D points supposed to be on the surface. """ point1_2d = self.point3d_to_2d(point1_3d) point2_2d = self.point3d_to_2d(point2_3d) return self.geodesic_distance_from_points2d(point1_2d, point2_2d)
[docs] def point_projection(self, point3d): """ Returns the projection of the point on the surface. :param point3d: Point to project. :type point3d: design3d.Point3D :return: A point on the surface :rtype: design3d.Point3D """ return self.point2d_to_3d(self.point3d_to_2d(point3d))
[docs] def point_distance(self, point3d: design3d.Point3D): """ Calculates the minimal distance from a given point and the surface. :param point3d: point to verify distance. :type point3d: design3d.Point3D :return: point distance to the surface. :rtype: float """ proj_point = self.point_projection(point3d) return proj_point.point_distance(point3d)
[docs] def edge_intersections(self, edge): """ Gets intersections between a Surface 3D, and an edge 3D. :param edge: any 3D edge. :return: list of points. """ intersections = [] method_name = f'{edge.__class__.__name__.lower()[:-2]}_intersections' if hasattr(self, method_name): intersections = getattr(self, method_name)(edge) return intersections
[docs] def contour_intersections(self, contour3d: wires.Contour3D): """ Gets intersections between a contour 3D and a Surface 3D. :param contour3d: other contour get intersections with. :return: list of intersecting points. """ outer_contour_intersections_with_plane = [] for primitive in contour3d.primitives: primitive_plane_intersections = self.edge_intersections(primitive) for primitive_plane_intersection in primitive_plane_intersections: if not primitive_plane_intersection.in_list(outer_contour_intersections_with_plane): outer_contour_intersections_with_plane.append(primitive_plane_intersection) return outer_contour_intersections_with_plane
[docs] def is_singularity_point(self, *args, **kwargs): """Verifies if point is on the surface singularity.""" return False
[docs] @staticmethod def is_undefined_brep(*args): """Verifies if the edge is contained within the periodicity boundary.""" return False
[docs] def surface_intersections(self, other_surface: 'Surface3D'): """ Gets intersections between two surfaces. :param other_surface: other surface to get intersections with. :return: a list containing all intersections between the two surfaces 3d. """ method_name = f'{other_surface.__class__.__name__.lower()[:-2]}_intersections' if hasattr(self, method_name): return getattr(self, method_name)(other_surface) method_name = f'{self.__class__.__name__.lower()[:-2]}_intersections' if hasattr(other_surface, method_name): return getattr(other_surface, method_name)(self) raise NotImplementedError(f'No method available for calculating intersections between {self.__class__} and ' f'{other_surface.__class__}')
[docs] def line_intersections(self, line: curves.Line3D): """Gets intersections between a line and a Surface 3D.""" raise NotImplementedError(f'line_intersections method not implemented by {self.__class__.__name__}')
[docs] def frame_mapping(self, frame, side: str): """Frame mapping for Surface 3D.""" raise NotImplementedError(f'frame_mapping method not implemented by {self.__class__.__name__}')
[docs] def linesegment_intersections(self, linesegment3d: edges.LineSegment3D, abs_tol: float = 1e-6): """ Calculates the intersection points between a 3D line segment and a surface 3D. The method calculates the intersection points between a 3D line segment and a 3d Surface by first finding the intersection points between the infinite line containing the line segment and the Surface, and then filtering out the points that are not within the line segment. It returns a list of intersection points, which can be empty if there are no intersections. The intersection points are represented as 3D points using the `design3d.Point3D` class. Note: The method assumes that the line segment and the Surface are in the same coordinate system. :param linesegment3d: The 3D line segment object to intersect with the Surface. :type linesegment3d: edges.LineSegment3D. :param abs_tol: tolerance. :type abs_tol: float. :return: A list of intersection points between the line segment and the Surface. The list may be empty if there are no intersections. :rtype: List[design3d.Point3D]: """ line_intersections = self.line_intersections(linesegment3d.line) linesegment_intersections = [inters for inters in line_intersections if linesegment3d.point_belongs(inters, abs_tol)] return linesegment_intersections
[docs] def plane_intersections(self, plane3d: 'Plane3D'): """Gets intersections between a line and a Surface 3D.""" raise NotImplementedError(f'line_intersections method not implemented by {self.__class__.__name__}')
[docs] def curve_intersections(self, curve): """ Calculates the intersections between a conical surface and a curve 3D. :param curve: other circle to verify intersections. :return: a list of intersection points, if there exists any. """ if not self.frame.origin.is_close(design3d.O3D) or not self.frame.w.is_close(design3d.Z3D): local_surface = self.frame_mapping(self.frame, 'new') local_curve = curve.frame_mapping(self.frame, 'new') local_intersections = local_surface.curve_intersections(local_curve) global_intersections = [] for intersection in local_intersections: global_intersections.append(self.frame.local_to_global_coordinates(intersection)) return global_intersections curve_plane = Plane3D(curve.frame) curve_plane_intersections = self.plane_intersections(curve_plane) if not curve_plane_intersections: return [] intersections = [] for curve_plane_intersection in curve_plane_intersections: inters = curve_plane_intersection.intersections(curve) for intersection in inters: if not intersection.in_list(intersections): intersections.append(intersection) return intersections
[docs] def circle_intersections(self, circle: curves.Circle3D): """ Calculates the intersections between a surface 3d and a Circle 3D. :param circle: other circle to verify intersections. :return: a list of intersection points, if there exists any. """ return self.curve_intersections(circle)
[docs] def ellipse_intersections(self, ellipse: curves.Ellipse3D): """ Calculates the intersections between a conical surface and an ellipse 3D. :param ellipse: other ellipse to verify intersections. :return: a list of intersection points, if there exists any. """ return self.curve_intersections(ellipse)
[docs] def hyperbola_intersections(self, hyperbola: curves.Hyperbola3D): """ Calculates the intersections between a conical surface and a hyperbola 3D. :param hyperbola: other hyperbola to verify intersections. :return: a list of intersection points, if there exists any. """ return self.curve_intersections(hyperbola)
[docs] def parabola_intersections(self, parabola: curves.Parabola3D): """ Calculates the intersections between a conical surface and a parabola 3D. :param parabola: other parabola to verify intersections. :return: a list of intersection points, if there exists any. """ return self.curve_intersections(parabola)
[docs] def fullarc_intersections(self, fullarc: edges.FullArc3D): """ Calculates the intersections between a conical surface and a full arc 3D. :param fullarc: other fullarc to verify intersections. :return: a list of intersection points, if there exists any. """ return self.curve_intersections(fullarc.circle)
[docs] def arc_intersections(self, arc3d: edges.Arc3D): """ Calculates the intersections between a conical surface and an arc 3D. :param arc3d: other arc to verify intersections. :return: a list of intersection points, if there exists any. """ circle_intersections = self.curve_intersections(arc3d.circle) intersections = [] for intersection in circle_intersections: if arc3d.point_belongs(intersection): intersections.append(intersection) return intersections
[docs] def fullarcellipse_intersections(self, fullarcellipse: edges.FullArcEllipse3D): """ Calculates the intersections between a conical surface and a fullarcellipse 3D. :param fullarcellipse: other fullarcellipse to verify intersections. :return: a list of intersection points, if there exists any. """ return self.ellipse_intersections(fullarcellipse.ellipse)
[docs] def arcellipse_intersections(self, arcellipse: edges.ArcEllipse3D): """ Calculates the intersections between a conical surface and an arcellipse 3D. :param arcellipse: other arcellipse to verify intersections. :return: a list of intersection points, if there exists any. """ ellipse_intersections = self.curve_intersections(arcellipse.ellipse) intersections = [] for intersection in ellipse_intersections: if arcellipse.point_belongs(intersection): intersections.append(intersection) return intersections
[docs] def brep_connectivity_check(self, brep: wires.Contour2D, tol: float = 1e-6) -> bool: """Checks the topology of 2D BREP in 3D space.""" if len(brep.primitives) == 2 and brep.primitives[0].direction_independent_is_close(brep.primitives[1]): return False if self.x_periodicity: distance = brep.primitives[-1].end.point_distance(brep.primitives[0].start) if distance >= (0.99 * self.x_periodicity): return False if self.y_periodicity: distance = brep.primitives[-1].end.point_distance(brep.primitives[0].start) if distance >= (0.99 * self.y_periodicity): return False for prim1, prim2 in zip(brep.primitives, brep.primitives[1:] + [brep.primitives[0]]): end = self.point2d_to_3d(prim1.end) start = self.point2d_to_3d(prim2.start) if not end.is_close(start, tol): return False return True
[docs] def is_degenerated_brep(self, *args): """ An edge is said to be degenerated when it corresponds to a single 3D point. """ return False
[docs] def point_belongs(self, point3d, abs_tol: float = 1e-6): """ Verifies if point is on Toroidal Surface 3D. :param point3d: other point. :param abs_tol: tolerance. :return: True or False. """ if self.point_distance(point3d) < abs_tol: return True return False
[docs] class Plane3D(Surface3D): """ Defines a plane 3d. :param frame: u and v of frame describe the plane, w is the normal """ face_class = 'PlaneFace3D' def __init__(self, frame: design3d.Frame3D, name: str = ''): Surface3D.__init__(self, frame=frame, name=name) def __hash__(self): return hash(('plane 3d', self.frame)) def __eq__(self, other_plane): if other_plane.__class__.__name__ != self.__class__.__name__: return False return self.frame == other_plane.frame @property def normal(self): """Gets the plane normal vector.""" return self.frame.w
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a Plane3D. :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 Plane3D object. :rtype: :class:`design3d.faces.Plane3D` """ frame = object_dict[arguments[1]] frame = frame.normalize() return cls(frame, arguments[0][1:-1])
[docs] def to_step(self, current_id): """ Converts the object to a STEP representation. :param current_id: The ID of the last written primitive. :type current_id: int :return: The STEP representation of the object and the last ID. :rtype: tuple[str, list[int]] """ content, frame_id = self.frame.to_step(current_id) plane_id = frame_id + 1 content += f"#{plane_id} = PLANE('{self.name}',#{frame_id});\n" return content, [plane_id]
[docs] @classmethod def from_3_points(cls, *args, name: str = ''): """ Point 1 is used as origin of the plane. """ point1, point2, point3 = args vector1 = point2 - point1 vector1 = vector1.to_vector() vector2 = point3 - point1 vector2 = vector2.to_vector() vector1 = vector1.unit_vector() vector2 = vector2.unit_vector() normal = vector1.cross(vector2) normal = normal.unit_vector() frame = design3d.Frame3D(point1, vector1, normal.cross(vector1), normal) return cls(frame, name=name)
[docs] @classmethod def from_normal(cls, point, normal, name: str = ''): """Creates a Plane 3D form a point and a normal vector.""" v1 = normal.deterministic_unit_normal_vector() v2 = v1.cross(normal) return cls(design3d.Frame3D(point, v1, v2, normal), name=name)
[docs] @classmethod def from_plane_vectors(cls, plane_origin: design3d.Point3D, plane_x: design3d.Vector3D, plane_y: design3d.Vector3D, name: str = ''): """ Initializes a 3D plane object with a given plane origin and plane x and y vectors. :param plane_origin: A design3d.Point3D representing the origin of the plane. :param plane_x: A design3d.Vector3D representing the x-axis of the plane. :param plane_y: A design3d.Vector3D representing the y-axis of the plane. :param name: object's name. :return: A Plane3D object initialized from the provided plane origin and plane x and y vectors. """ normal = plane_x.cross(plane_y) return cls(design3d.Frame3D(plane_origin, plane_x, plane_y, normal), name=name)
[docs] @classmethod def from_points(cls, points, name: str = ''): """ Returns a 3D plane that goes through the 3 first points on the list. Why for more than 3 points we only do some check and never raise error? """ if len(points) < 3: raise ValueError if len(points) == 3: return cls.from_3_points(points[0], points[1], points[2], name=name) points = [p.copy() for p in points] indexes_to_del = [] for i, point in enumerate(points[1:]): if point.is_close(points[0]): indexes_to_del.append(i) for index in indexes_to_del[::-1]: del points[index + 1] origin = points[0] vector1 = points[1] - origin vector1 = vector1.unit_vector() vector2_min = points[2] - origin vector2_min = vector2_min.unit_vector() dot_min = abs(vector1.dot(vector2_min)) for point in points[3:]: vector2 = point - origin vector2 = vector2.unit_vector() dot = abs(vector1.dot(vector2)) if dot < dot_min: vector2_min = vector2 dot_min = dot return cls.from_3_points(origin, vector1 + origin, vector2_min + origin, name=name)
[docs] def angle_between_planes(self, plane2): """ Get angle between 2 planes. :param plane2: the second plane. :return: the angle between the two planes. """ angle = math.acos(self.frame.w.dot(plane2.frame.w)) return angle
[docs] def point_belongs(self, point3d, abs_tol: float = 1e-6): """ Return if the point belongs to the plane at a tolerance of 1e-6. """ if math.isclose(self.frame.w.dot(point3d - self.frame.origin), 0, abs_tol=abs_tol): return True return False
[docs] def point_distance(self, point3d): """ Calculates the distance of a point to plane. :param point3d: point to verify distance. :return: a float, point distance to plane. """ return d3d_common_operations.get_plane_point_distance(self.frame, point3d)
[docs] def line_intersections(self, line): """ Find the intersection with a line. :param line: Line to evaluate the intersection :type line: :class:`edges.Line` :return: ADD DESCRIPTION :rtype: List[design3d.Point3D] """ return d3d_utils_intersections.get_plane_line_intersections(self.frame, line)
[docs] def linesegment_intersections(self, linesegment3d: edges.LineSegment3D, abs_tol: float = 1e-6) \ -> List[design3d.Point3D]: """ Gets the intersections of a plane a line segment 3d. :param linesegment3d: other line segment. :param abs_tol: tolerance allowed. :return: a list with the intersecting point. """ return d3d_utils_intersections.get_plane_linesegment_intersections(self.frame, linesegment3d, abs_tol)
[docs] def bsplinecurve_intersections(self, bspline_curve): """ Calculates the intersections between a Plane 3D and a Bspline Curve 3D. :param bspline_curve: bspline_curve to verify intersections. :return: list of intersections: List[design3d.Point3D]. """ return d3d_utils_intersections.get_bsplinecurve_intersections(self, bspline_curve)
[docs] def equation_coefficients(self): """ Returns the a,b,c,d coefficient from equation ax+by+cz+d = 0. """ return d3d_common_operations.get_plane_equation_coefficients(self.frame)
[docs] def plane_intersections(self, plane3d): """ Computes intersection points between two Planes 3D. """ plane_intersections = d3d_utils_intersections.get_two_planes_intersections(self.frame, plane3d.frame) if plane_intersections: return [curves.Line3D(plane_intersections[0], plane_intersections[1])] return []
[docs] def cylindricalsurface_intersections(self, cylindrical_surface: 'CylindricalSurface3D'): """ Gets intersections between plane and cylindrical surface. :param cylindrical_surface: cylindrical surface to get intersections with. :return: List containing all intersections between plane and cylindrical surface. """ return cylindrical_surface.plane_intersections(self)
[docs] def is_coincident(self, plane2, abs_tol: float = 1e-6): """ Verifies if two planes are parallel and coincident. """ if not isinstance(self, plane2.__class__): return False if self.is_parallel(plane2, abs_tol): if plane2.point_belongs(self.frame.origin, abs_tol): return True return False
[docs] def is_parallel(self, plane2, abs_tol: float = 1e-6): """ Verifies if two planes are parallel. """ if self.frame.w.is_colinear_to(plane2.frame.w, abs_tol): return True return False
[docs] @classmethod def plane_between_two_planes(cls, plane1, plane2, name: str = ''): """ Calculates a plane between two other planes. :param plane1: plane1. :param plane2: plane2. :param name: object's name. :return: resulting plane. """ plane1_plane2_intersection = plane1.plane_intersections(plane2)[0] u = plane1_plane2_intersection.unit_direction_vector() v = plane1.frame.w + plane2.frame.w v = v.unit_vector() w = u.cross(v) point = (plane1.frame.origin + plane2.frame.origin) / 2 return cls(design3d.Frame3D(point, u, w, v), name=name)
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ Plane3D rotation. :param center: rotation center :param axis: rotation axis :param angle: angle rotation :return: a new rotated Plane3D """ new_frame = self.frame.rotation(center=center, axis=axis, angle=angle) return Plane3D(new_frame)
[docs] def translation(self, offset: design3d.Vector3D): """ Plane3D translation. :param offset: translation vector :return: A new translated Plane3D """ new_frame = self.frame.translation(offset) return Plane3D(new_frame)
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new Frame3D. :param frame: Frame of reference :type frame: `design3d.Frame3D` :param side: 'old' or 'new' """ new_frame = self.frame.frame_mapping(frame, side) return Plane3D(new_frame, self.name)
[docs] def copy(self, deep=True, memo=None): """Creates a copy of the plane.""" new_frame = self.frame.copy(deep, memo) return Plane3D(new_frame, self.name)
[docs] def plane_grid(self, grid_size: int, length: float = 1.): """ Plane's grid. """ plane_grid = [] for i in range(grid_size): for v1, v2 in [(self.frame.u, self.frame.v), (self.frame.v, self.frame.u)]: start = self.frame.origin - 0.5 * length * v1 + (-0.5 + i / (grid_size - 1)) * length * v2 end = self.frame.origin + 0.5 * length * v1 + (-0.5 + i / (grid_size - 1)) * length * v2 plane_grid.append(edges.LineSegment3D(start, end)) return plane_grid
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey'), length: float = 1., **kwargs): """ Plot the cylindrical surface in the local frame normal direction. :param ax: Matplotlib Axes3D object to plot on. If None, create a new figure. :type ax: Axes3D or None :param edge_style: edge styles. :type edge_style: EdgeStyle. :param length: plotted length :type length: float :return: Matplotlib Axes3D object containing the plotted wire-frame. :rtype: Axes3D """ grid_size = 10 if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') ax.set_aspect('auto') self.frame.plot(ax=ax, ratio=length) for edge in self.plane_grid(grid_size, length): edge.plot(ax, edge_style=edge_style) return ax
[docs] def point2d_to_3d(self, point2d): """ Converts a 2D parametric point into a 3D point on the surface. """ return point2d.to_3d(self.frame.origin, self.frame.u, self.frame.v)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the plane. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the plane. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the plane in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ center = np.asarray(self.frame.origin) x = np.asarray([self.frame.u[0], self.frame.u[1], self.frame.u[2]]) y = np.asarray([self.frame.v[0], self.frame.v[1], self.frame.v[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] v_values = points[:, 1] return center + u_values * x + v_values * y
[docs] def point3d_to_2d(self, point3d): """ Converts a 3D point into a 2D parametric point. """ return point3d.to_2d(self.frame.origin, self.frame.u, self.frame.v)
[docs] def contour2d_to_3d(self, contour2d, return_primitives_mapping: bool = False): """ Transforms a Contour2D in the parametric domain of the surface into a Contour3D in Cartesian coordinate. :param contour2d: The contour to be transformed. :type contour2d: :class:`wires.Contour2D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 3D contour object. :rtype: :class:`wires.Contour3D` """ contour3d = contour2d.to_3d(self.frame.origin, self.frame.u, self.frame.v) if return_primitives_mapping: primitives_mapping = dict(zip(contour2d.primitives, contour3d.primitives)) return contour3d, primitives_mapping return contour3d
[docs] def contour3d_to_2d(self, contour3d, return_primitives_mapping: bool = False): """ Transforms a Contour3D into a Contour2D in the parametric domain of the surface. :param contour3d: The contour to be transformed. :type contour3d: :class:`wires.Contour3D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 2D contour object. :rtype: :class:`wires.Contour2D` """ primitives2d = [] primitives_mapping = {} for primitive3d in contour3d.primitives: method_name = f'{primitive3d.__class__.__name__.lower()}_to_2d' if hasattr(self, method_name): primitives = getattr(self, method_name)(primitive3d) if primitives is None: continue self.update_primitives_mapping(primitives_mapping, primitives, primitive3d) primitives2d.extend(primitives) else: primitive = primitive3d.to_2d(self.frame.origin, self.frame.u, self.frame.v) if primitive is None: continue self.update_primitives_mapping(primitives_mapping, [primitive], primitive3d) primitives2d.append(primitive) if return_primitives_mapping: return wires.Contour2D(primitives2d), primitives_mapping return wires.Contour2D(primitives2d)
[docs] def arc3d_to_2d(self, arc3d): """Converts primitive from 3D cartesian space to surface parametric space.""" arc = None if arc3d.circle.frame.w.is_colinear_to(self.frame.w, 1e-5): arc = [arc3d.to_2d(self.frame.origin, self.frame.u, self.frame.v)] else: start = self.point3d_to_2d(arc3d.start) end = self.point3d_to_2d(arc3d.end) if not start.is_close(end): arc = [edges.LineSegment2D(start, end)] return arc
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Converts a 3D B-Spline in spatial domain into a 2D B-Spline in parametric domain. :param bspline_curve3d: The B-Spline curve to perform the transformation. :type bspline_curve3d: edges.BSplineCurve3D :return: A 2D B-Spline. :rtype: edges.BSplineCurve2D """ control_points = [self.point3d_to_2d(p) for p in bspline_curve3d.control_points] return [edges.BSplineCurve2D( bspline_curve3d.degree, control_points=control_points, knot_multiplicities=bspline_curve3d.knot_multiplicities, knots=bspline_curve3d.knots, weights=bspline_curve3d.weights)]
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Converts a 2D B-Spline in parametric domain into a 3D B-Spline in spatial domain. :param bspline_curve2d: The B-Spline curve to perform the transformation. :type bspline_curve2d: edges.BSplineCurve2D :return: A 3D B-Spline. :rtype: edges.BSplineCurve3D """ control_points = [self.point2d_to_3d(point) for point in bspline_curve2d.control_points] return [edges.BSplineCurve3D( bspline_curve2d.degree, control_points=control_points, knot_multiplicities=bspline_curve2d.knot_multiplicities, knots=bspline_curve2d.knots, weights=bspline_curve2d.weights)]
[docs] def rectangular_cut(self, x1: float, x2: float, y1: float, y2: float, name: str = ''): """Deprecated method, Use PlaneFace3D from_surface_rectangular_cut method.""" raise AttributeError('Use PlaneFace3D from_surface_rectangular_cut method')
[docs] def u_iso(self, u: float) -> curves.Line3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A line 3D :rtype: :class:`curves.Line3D` """ point_at_u = self.point2d_to_3d(design3d.Point2D(u, 0.0)) return curves.Line3D.from_point_and_vector(point_at_u, self.frame.v)
[docs] def v_iso(self, v: float) -> curves.Line3D: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A line 3D :rtype: :class:`curves.Line3D` """ point_at_v = self.point2d_to_3d(design3d.Point2D(0.0, v)) return curves.Line3D.from_point_and_vector(point_at_v, self.frame.u)
[docs] def normal_at_point(self, point): """ Gets Normal vector at a given point on the surface. :param point: point on the surface. :return: """ if not self.point_belongs(point): raise ValueError(f'Point {point} not on this surface.') return self.frame.w
PLANE3D_OXY = Plane3D(design3d.OXYZ) PLANE3D_OYZ = Plane3D(design3d.OYZX) PLANE3D_OZX = Plane3D(design3d.OZXY) PLANE3D_OXZ = Plane3D(design3d.Frame3D(design3d.O3D, design3d.X3D, design3d.Z3D, design3d.Y3D))
[docs] class UPeriodicalSurface(Surface3D): """ Abstract class for surfaces with two-pi periodicity that creates some problems. """
[docs] def point2d_to_3d(self, point2d): """ Abstract method. """ raise NotImplementedError(f'point2d_to_3d is abstract and should be implemented in {self.__class__.__name__}')
[docs] def point3d_to_2d(self, point3d): """ Abstract method. Convert a 3D point to a 2D parametric point. :param point3d: The 3D point to convert, represented by 3 coordinates (x, y, z). :type point3d: `design3d.Point3D` :return: NotImplementedError: If the method is not implemented in the subclass. """ raise NotImplementedError(f'point3d_to_2d is abstract and should be implemented in {self.__class__.__name__}')
[docs] def v_iso(self, v): """ Abstract method. """ raise NotImplementedError(f'v_iso is abstract and should be implemented in {self.__class__.__name__}')
def _align_contours(self, inner_contour, theta_contours, z_outer_contour, z_inner_contour): """ Helper function to align contours' BREP on periodical surfaces that need to be connected. """ outer_contour_theta, inner_contour_theta = theta_contours overlapping_theta, outer_contour_side, inner_contour_side, side = self._get_overlapping_theta( outer_contour_theta, inner_contour_theta) line = curves.Line2D(design3d.Point2D(overlapping_theta, z_outer_contour), design3d.Point2D(overlapping_theta, z_inner_contour)) cutted_contours = inner_contour.split_by_line(line) number_contours = len(cutted_contours) if number_contours == 2: contour1, contour2 = cutted_contours increasing_theta = inner_contour_theta[0] < inner_contour_theta[1] # side = 0 --> left side = 1 --> right if (not side and increasing_theta) or ( side and not increasing_theta): theta_offset = outer_contour_theta[outer_contour_side] - contour2.primitives[0].start.x contour2_positionned = contour2.translation(offset=design3d.Vector2D(theta_offset, 0)) theta_offset = contour2_positionned.primitives[-1].end.x - contour1.primitives[0].start.x contour1_positionned = contour1.translation(offset=design3d.Vector2D(theta_offset, 0)) else: theta_offset = outer_contour_theta[outer_contour_side] - contour1.primitives[-1].end.x contour1_positionned = contour1.translation(offset=design3d.Vector2D(theta_offset, 0)) theta_offset = contour1_positionned.primitives[0].start.x - contour2.primitives[-1].end.x contour2_positionned = contour2.translation(offset=design3d.Vector2D(theta_offset, 0)) old_innner_contour_positioned = wires.Wire2D(contour2_positionned.primitives + contour1_positionned.primitives).order_wire(tol=1e-2) elif number_contours == 1: theta_offset = outer_contour_theta[outer_contour_side] - inner_contour_theta[inner_contour_side] translation_vector = design3d.Vector2D(theta_offset, 0) old_innner_contour_positioned = cutted_contours[0].translation(offset=translation_vector) else: raise NotImplementedError return old_innner_contour_positioned @staticmethod def _get_closing_points(old_outer_contour_positioned, old_innner_contour_positioned): """ Helper function to get points to connect contours with line segments. """ point1 = old_outer_contour_positioned.primitives[0].start point2 = old_outer_contour_positioned.primitives[-1].end point3 = old_innner_contour_positioned.primitives[0].start point4 = old_innner_contour_positioned.primitives[-1].end outer_contour_direction = point1.x < point2.x inner_contour_direction = point3.x < point4.x if outer_contour_direction == inner_contour_direction: old_innner_contour_positioned = old_innner_contour_positioned.invert() point3 = old_innner_contour_positioned.primitives[0].start point4 = old_innner_contour_positioned.primitives[-1].end if not math.isclose(point2.x, point3.x, abs_tol=1e-4) or \ not math.isclose(point4.x, point1.x, abs_tol=1e-4): ideal_x = [] delta = math.inf found = False for x1 in [point2.x, point3.x]: for x2 in [point4.x, point1.x]: delta_x = abs(abs(x1 - x2) - design3d.TWO_PI) if delta_x == 0.0: ideal_x = [x1, x2] found = True break if delta_x < delta: delta = delta_x ideal_x = [x1, x2] if found: break x1, x2 = ideal_x point2.x = x1 point3.x = x1 point4.x = x2 point1.x = x2 return point1, point2, point3, point4
[docs] def connect_contours(self, outer_contour, inner_contours): """ Repair contours on parametric domain. :param outer_contour: Outer contour 2D. :type inner_contours: wires.Contour2D :param inner_contours: List of 2D contours. :type inner_contours: list """ new_inner_contours = [] point1 = outer_contour.primitives[0].start point2 = outer_contour.primitives[-1].end theta1, z1 = point1 theta2, _ = point2 new_outer_contour = outer_contour for inner_contour in inner_contours: theta3, z3 = inner_contour.primitives[0].start theta4, _ = inner_contour.primitives[-1].end if not inner_contour.is_ordered(): # Contours are aligned if (math.isclose(theta1, theta3, abs_tol=1e-3) and math.isclose(theta2, theta4, abs_tol=1e-3)) \ or (math.isclose(theta1, theta4, abs_tol=1e-3) and math.isclose(theta2, theta3, abs_tol=1e-3)): old_innner_contour_positioned = inner_contour else: old_innner_contour_positioned = self._align_contours(inner_contour, [[theta1, theta2], [theta3, theta4]], z1, z3) point1, point2, point3, point4 = self._get_closing_points(outer_contour, old_innner_contour_positioned) closing_linesegment1 = edges.LineSegment2D(point2, point3) closing_linesegment2 = edges.LineSegment2D(point4, point1) new_outer_contour_primitives = outer_contour.primitives + [closing_linesegment1] + \ old_innner_contour_positioned.primitives + [closing_linesegment2] new_outer_contour = wires.Contour2D(primitives=new_outer_contour_primitives) if not new_outer_contour.is_ordered(): try: new_outer_contour = new_outer_contour.order_contour( tol=min(1e-2, 0.1 * closing_linesegment1.length(), 0.1 * closing_linesegment2.length())) except NotImplementedError: pass else: new_inner_contours.append(inner_contour) return new_outer_contour, new_inner_contours
@staticmethod def _get_overlapping_theta(outer_contour_startend_theta, inner_contour_startend_theta): """ Find overlapping theta domain between two contours on periodical Surfaces. """ oc_xmin_index, outer_contour_xmin = min(enumerate(outer_contour_startend_theta), key=lambda x: x[1]) oc_xmax_index, outer_contour_xman = max(enumerate(outer_contour_startend_theta), key=lambda x: x[1]) ic_xmin_index, inner_contour_xmin = min(enumerate(inner_contour_startend_theta), key=lambda x: x[1]) ic_xmax_index, inner_contour_xmax = max(enumerate(inner_contour_startend_theta), key=lambda x: x[1]) # check if tetha3 or theta4 is in [theta1, theta2] interval overlap = outer_contour_xmin <= inner_contour_xmax and outer_contour_xman >= inner_contour_xmin if overlap: if inner_contour_xmin < outer_contour_xmin: overlapping_theta = outer_contour_startend_theta[oc_xmin_index] side = 0 return overlapping_theta, oc_xmin_index, ic_xmin_index, side overlapping_theta = outer_contour_startend_theta[oc_xmax_index] side = 1 return overlapping_theta, oc_xmax_index, ic_xmax_index, side # if not direct intersection -> find intersection at periodicity if inner_contour_xmin < outer_contour_xmin: overlapping_theta = outer_contour_startend_theta[oc_xmin_index] - 2 * math.pi side = 0 return overlapping_theta, oc_xmin_index, ic_xmin_index, side overlapping_theta = outer_contour_startend_theta[oc_xmax_index] + 2 * math.pi side = 1 return overlapping_theta, oc_xmax_index, ic_xmax_index, side def _reference_points(self, edge): """ Helper function to return points of reference on the edge to fix some parametric periodical discontinuities. """ length = edge.length() point_after_start = self.point3d_to_2d(edge.point_at_abscissa(0.01 * length)) point_before_end = self.point3d_to_2d(edge.point_at_abscissa(0.98 * length)) theta3, _ = point_after_start theta4, _ = point_before_end if abs(theta3) == math.pi or abs(theta3) == 0.5 * math.pi: point_after_start = self.point3d_to_2d(edge.point_at_abscissa(0.02 * length)) if abs(theta4) == math.pi or abs(theta4) == 0.5 * math.pi: point_before_end = self.point3d_to_2d(edge.point_at_abscissa(0.97 * length)) return point_after_start, point_before_end def _verify_start_end_angles(self, edge, theta1, theta2): """ Verify if there is some incoherence with start and end angles. If so, return fixed angles. """ length = edge.length() theta3, _ = self.point3d_to_2d(edge.point_at_abscissa(0.001 * length)) # make sure that the reference angle is not undefined if abs(theta3) == math.pi or abs(theta3) == 0.5 * math.pi: theta3, _ = self.point3d_to_2d(edge.point_at_abscissa(0.002 * length)) # Verify if theta1 or theta2 point should be -pi because atan2() -> ]-pi, pi] # And also atan2 discontinuity in 0.5 * math.pi if math.isclose(abs(theta1), math.pi, abs_tol=1e-4) or abs(theta1) == 0.5 * math.pi: theta1 = repair_start_end_angle_periodicity(theta1, theta3) if abs(theta2) == math.pi or abs(theta2) == 0.5 * math.pi: theta4, _ = self.point3d_to_2d(edge.point_at_abscissa(0.98 * length)) # make sure that the reference angle is not undefined if math.isclose(abs(theta2), math.pi, abs_tol=1e-4) or abs(theta4) == 0.5 * math.pi: theta4, _ = self.point3d_to_2d(edge.point_at_abscissa(0.97 * length)) theta2 = repair_start_end_angle_periodicity(theta2, theta4) return theta1, theta2 def _helper_fix_angle_discontinuity(self, points, index_angle_discontinuity, i): sign = round(points[index_angle_discontinuity - 1][i] / abs(points[index_angle_discontinuity - 1][i]), 2) if i == 0: points = [p + design3d.Point2D(sign * design3d.TWO_PI, 0) if i >= index_angle_discontinuity else p for i, p in enumerate(points)] else: points = [p + design3d.Point2D(0, sign * design3d.TWO_PI) if i >= index_angle_discontinuity else p for i, p in enumerate(points)] return points def _fix_angle_discontinuity_on_discretization_points(self, points, indexes_angle_discontinuity, direction): i = 0 if direction == "x" else 1 if len(indexes_angle_discontinuity) == 1: index_angle_discontinuity = indexes_angle_discontinuity[0] points = self._helper_fix_angle_discontinuity(points, index_angle_discontinuity, i) else: stack = deque(indexes_angle_discontinuity) while stack: index_angle_discontinuity = stack.popleft() if stack: next_angle_discontinuity_index = stack[0] temp_points = points[:next_angle_discontinuity_index] temp_points = self._helper_fix_angle_discontinuity(temp_points, index_angle_discontinuity, i) points = temp_points + points[next_angle_discontinuity_index:] else: temp_points = points points = self._helper_fix_angle_discontinuity(temp_points, index_angle_discontinuity, i) _, indexes_angle_discontinuity = angle_discontinuity([point.x for point in points]) stack = deque(indexes_angle_discontinuity) return points def _helper_arc3d_to_2d_periodicity_verifications(self, arc3d, start): """ Verifies if arc 3D contains discontinuity and undefined start/end points on parametric domain. """ point_theta_discontinuity = self.point2d_to_3d(design3d.Point2D(math.pi, start.y)) discontinuity = arc3d.point_belongs(point_theta_discontinuity) and not \ arc3d.is_point_edge_extremity(point_theta_discontinuity) undefined_start_theta = arc3d.start.is_close(point_theta_discontinuity) undefined_end_theta = arc3d.end.is_close(point_theta_discontinuity) return discontinuity, undefined_start_theta, undefined_end_theta
[docs] def linesegment3d_to_2d(self, linesegment3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. For cylindrical or conical surfaces, a line segment in 3D space is typically projected onto the 2D parametric space as a vertical line segment. This is because a 3D line that lies on a cylindrical or conical surface corresponds to a generatrix of the surface, and it extends along the height of the surface without bending or deviating in the other directions. Therefore, the BREP of a line segment on cylindrical or conical surface is a vertical line segment. """ start = self.point3d_to_2d(linesegment3d.start) end = self.point3d_to_2d(linesegment3d.end) _, _, z1 = self.frame.global_to_local_coordinates(linesegment3d.start) _, _, z2 = self.frame.global_to_local_coordinates(linesegment3d.end) if math.isclose(z1, z2, rel_tol=0.005): # special case when there is a small line segment that should be a small arc of circle instead return [edges.LineSegment2D(start, end)] if start.x != end.x: end = design3d.Point2D(start.x, end.y) if not start.is_close(end): return [edges.LineSegment2D(start, end)] return None
[docs] def arc3d_to_2d(self, arc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. The BREP of an arc of circle on a cylindrical or a conical surface is a horizontal line segment. """ start = self.point3d_to_2d(arc3d.start) end = self.point3d_to_2d(arc3d.end) point_after_start, point_before_end = self._reference_points(arc3d) discontinuity, undefined_start_theta, undefined_end_theta = self._helper_arc3d_to_2d_periodicity_verifications( arc3d, start) start, end = d3d_parametric.arc3d_to_cylindrical_coordinates_verification( [start, end], [undefined_start_theta, undefined_end_theta], [point_after_start.x, point_before_end.x], discontinuity) return [edges.LineSegment2D(start, end)]
[docs] def fullarc3d_to_2d(self, fullarc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. The BREP of a circle on a cylindrical or a conical surface is a horizontal line segment with length of two pi. """ start = self.point3d_to_2d(fullarc3d.start) end = self.point3d_to_2d(fullarc3d.end) if self.frame.w.is_colinear_to(fullarc3d.circle.normal): normal_dot_product = self.frame.w.dot(fullarc3d.circle.normal) start, end = d3d_parametric.fullarc_to_cylindrical_coordinates_verification(start, end, normal_dot_product) return [edges.LineSegment2D(start, end)] raise NotImplementedError("This case must be treated in child class.")
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ n = bspline_curve3d.ctrlpts.shape[0] points3d = bspline_curve3d.discretization_points(number_points=n) points = [self.point3d_to_2d(point) for point in points3d] if self.is_singularity_point(bspline_curve3d.start) or self.is_singularity_point(bspline_curve3d.end): points = self._fix_start_end_singularity_point_at_parametric_domain(bspline_curve3d, points, points3d) theta1, z1 = points[0] theta2, z2 = points[-1] theta1, theta2 = self._verify_start_end_angles(bspline_curve3d, theta1, theta2) points[0] = design3d.Point2D(theta1, z1) points[-1] = design3d.Point2D(theta2, z2) theta_list = [point.x for point in points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") return [edges.BSplineCurve2D.from_points_interpolation(points, degree=bspline_curve3d.degree)]
[docs] def arcellipse3d_to_2d(self, arcellipse3d): """ Transformation of a 3D arc of ellipse to a 2D primitive in a cylindrical surface. """ points = [self.point3d_to_2d(p) for p in arcellipse3d.discretization_points(number_points=50)] theta1, z1 = points[0] theta2, z2 = points[-1] # theta3, _ = self.point3d_to_2d(arcellipse3d.point_at_abscissa(0.001 * length)) theta3, _ = points[1] # make sure that the reference angle is not undefined if abs(theta3) == math.pi: theta3, _ = points[1] # Verify if theta1 or theta2 point should be -pi because atan2() -> ]-pi, pi] if abs(theta1) == math.pi: theta1 = d3d_parametric.repair_start_end_angle_periodicity(theta1, theta3) if abs(theta2) == math.pi: theta4, _ = points[-2] # make sure that the reference angle is not undefined if abs(theta4) == math.pi: theta4, _ = points[-3] theta2 = d3d_parametric.repair_start_end_angle_periodicity(theta2, theta4) points[0] = design3d.Point2D(theta1, z1) points[-1] = design3d.Point2D(theta2, z2) theta_list = [point.x for point in points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") return [edges.BSplineCurve2D.from_points_interpolation(points, degree=2, name="parametric.arcellipse")]
[docs] def fullarcellipse3d_to_2d(self, fullarcellipse3d): """ Transformation of a 3D arc ellipse to 2D, in a cylindrical surface. """ points = [self.point3d_to_2d(p) for p in fullarcellipse3d.discretization_points(number_points=72)] start, end = points[0], points[-1] normal_dot_product = self.frame.w.dot(fullarcellipse3d.ellipse.normal) start, end = d3d_parametric.fullarc_to_cylindrical_coordinates_verification(start, end, normal_dot_product) theta1, z1 = start theta2, z2 = end theta1, theta2 = self._verify_start_end_angles(fullarcellipse3d, theta1, theta2) theta_list = [point.x for point in points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") points[0] = design3d.Point2D(theta1, z1) points[-1] = design3d.Point2D(theta2, z2) return [edges.BSplineCurve2D.from_points_interpolation(points, degree=3, name="parametric.fullarcellipse")]
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Is this right?. """ n = len(bspline_curve2d.control_points) points = [self.point2d_to_3d(p) for p in bspline_curve2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, bspline_curve2d.degree, centripetal=True)]
[docs] def linesegment2d_to_3d(self, linesegment2d): """ Converts a BREP line segment 2D onto a 3D primitive on the surface. """ if linesegment2d.name == "construction" or self.is_degenerated_brep(linesegment2d): return None theta1, param_z1 = linesegment2d.start theta2, param_z2 = linesegment2d.end start3d = self.point2d_to_3d(linesegment2d.start) end3d = self.point2d_to_3d(linesegment2d.end) if math.isclose(theta1, theta2, abs_tol=1e-4): if start3d.is_close(end3d): return None return [edges.LineSegment3D(start3d, end3d)] if math.isclose(param_z1, param_z2, abs_tol=1e-4): circle3d = self.v_iso(param_z1) if theta1 > theta2: circle3d = circle3d.reverse() return [circle3d.trim(start3d, end3d)] if start3d.is_close(end3d): return None n = int(54 * abs(theta2 - theta1)/math.pi) points = [self.point2d_to_3d(p) for p in linesegment2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, 7)]
[docs] @staticmethod def is_undefined_brep(edge): """Returns True if the edge is contained within the periodicity boundary.""" if isinstance(edge.simplify, edges.LineSegment2D) and \ edge.simplify.line.unit_direction_vector().is_colinear_to(design3d.Y2D) \ and math.isclose(abs(edge.start.x), math.pi, abs_tol=1e-6): return True return False
[docs] def is_degenerated_brep(self, *args): """ An edge is said to be degenerated when it corresponds to a single 3D point. """ edge = args[0] start3d = self.point2d_to_3d(edge.start) end3d = self.point2d_to_3d(edge.end) return bool(self.is_singularity_point(start3d) and start3d.is_close(end3d))
[docs] def fix_undefined_brep_with_neighbors(self, edge, previous_edge, next_edge): """Uses neighbors edges to fix edge contained within the periodicity boundary.""" delta_previous = previous_edge.end - edge.start delta_next = next_edge.start - edge.end if not self.is_undefined_brep(previous_edge) and \ math.isclose(delta_previous.norm(), self.x_periodicity, abs_tol=1e-3): edge = edge.translation(delta_previous) elif not self.is_undefined_brep(next_edge) and \ math.isclose(delta_next.norm(), self.x_periodicity, abs_tol=1e-3): edge = edge.translation(delta_next) elif (math.isclose(delta_previous.x, delta_next.x, abs_tol=1e-3) and math.isclose(abs(delta_previous.x), self.x_periodicity, abs_tol=1e-3)): edge = edge.translation(delta_next) return edge
def _fix_start_end_singularity_point_at_parametric_domain(self, edge3d, points, points3d): """ Helper function. Uses local discretization and line intersection with the tangent line at the point just before the undefined point on the BREP of the 3D edge to find the real values on parametric domain. """ def get_local_discretization_points(start_point, end_points): distance = start_point.point_distance(end_points) maximum_linear_distance_reference_point = 1e-4 if distance < maximum_linear_distance_reference_point: return [] number_points = max(int(distance / maximum_linear_distance_reference_point), 2) local_discretization = [self.point3d_to_2d(point) for point in edge3d.local_discretization( start_point, end_points, number_points)] return local_discretization def get_temp_edge2d(_points): theta_list = [point.x for point in _points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: _points = self._fix_angle_discontinuity_on_discretization_points(_points, indexes_theta_discontinuity, "x") if len(_points) == 2: edge2d = edges.LineSegment2D(_points[0], _points[1]) else: edge2d = edges.BSplineCurve2D.from_points_interpolation(_points, 2) return edge2d if self.is_singularity_point(points3d[0]): local_discretization_points = get_local_discretization_points(start_point=points3d[0], end_points=points3d[1]) if local_discretization_points: temp_points = local_discretization_points[1:] + points[2:] else: temp_points = points temp_edge2d = get_temp_edge2d(temp_points) singularity_lines = self.get_singularity_lines() if len(singularity_lines) > 1: singularity_line = min(singularity_lines, key=lambda x: x.point_distance(temp_points[0])) else: singularity_line = singularity_lines[0] points[0] = find_parametric_point_at_singularity(temp_edge2d, abscissa=0, singularity_line=singularity_line, domain=self.domain) if self.is_singularity_point(points3d[-1]): local_discretization_points = get_local_discretization_points(start_point=points3d[-2], end_points=points3d[-1]) if local_discretization_points: temp_points = points[:-2] + local_discretization_points[:-1] else: temp_points = points[:-1] temp_edge2d = get_temp_edge2d(temp_points) singularity_lines = self.get_singularity_lines() if len(singularity_lines) > 1: singularity_line = min(singularity_lines, key=lambda x: x.point_distance(temp_points[-1])) else: singularity_line = singularity_lines[0] points[-1] = find_parametric_point_at_singularity(temp_edge2d, abscissa=temp_edge2d.length(), singularity_line=singularity_line, domain=self.domain) return points
[docs] class UVPeriodicalSurface(UPeriodicalSurface): """ Abstract class for surfaces with two-pi periodicity in both u and v parametric directions. """
[docs] def point2d_to_3d(self, point2d): """ Abstract method. """ raise NotImplementedError(f'point2d_to_3d is abstract and should be implemented in {self.__class__.__name__}')
[docs] def point3d_to_2d(self, point3d): """ Abstract method. Convert a 3D point to a 2D parametric point. :param point3d: The 3D point to convert, represented by 3 coordinates (x, y, z). :type point3d: `design3d.Point3D` :return: NotImplementedError: If the method is not implemented in the subclass. """ raise NotImplementedError(f'point3d_to_2d is abstract and should be implemented in {self.__class__.__name__}')
[docs] def u_iso(self, u): """ Abstract method. """ raise NotImplementedError(f'u_iso is abstract and should be implemented in {self.__class__.__name__}')
[docs] def v_iso(self, v): """ Abstract method. """ raise NotImplementedError(f'u_iso is abstract and should be implemented in {self.__class__.__name__}')
[docs] def linesegment2d_to_3d(self, linesegment2d): """ Converts the parametric boundary representation into a 3D primitive. """ if linesegment2d.name == "construction" or self.is_degenerated_brep(linesegment2d): return None theta1, phi1 = linesegment2d.start theta2, phi2 = linesegment2d.end start3d = self.point2d_to_3d(linesegment2d.start) end3d = self.point2d_to_3d(linesegment2d.end) if math.isclose(theta1, theta2, abs_tol=1e-4): circle = self.u_iso(theta1) if phi1 > phi2: circle = circle.reverse() return [circle.trim(start3d, end3d)] if math.isclose(phi1, phi2, abs_tol=1e-4): circle = self.v_iso(phi1) if theta1 > theta2: circle = circle.reverse() return [circle.trim(start3d, end3d)] points = [self.point2d_to_3d(point2d) for point2d in linesegment2d.discretization_points(number_points=10)] return [edges.BSplineCurve3D.from_points_interpolation(points, degree=3).simplify]
[docs] class CylindricalSurface3D(UPeriodicalSurface): """ The local plane is defined by (theta, z). :param frame: frame.w is axis, frame.u is theta=0 frame.v theta=pi/2 :param frame: :param radius: Cylinder's radius :type radius: float """ face_class = 'CylindricalFace3D' x_periodicity = design3d.TWO_PI y_periodicity = None def __init__(self, frame, radius: float, name: str = ''): self.radius = radius UPeriodicalSurface.__init__(self, frame=frame, name=name) def __hash__(self): return hash((self.__class__.__name__, self.frame, self.radius)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.frame == other.frame and self.radius == other.radius: return True return False @property def u_domain(self): """The parametric domain of the surface in the U direction.""" return -math.pi, math.pi @property def v_domain(self): """The parametric domain of the surface in the V direction.""" return -math.inf, math.inf
[docs] def get_generatrices(self, number_lines: int = 30, length: float = 1): """ Retrieve line segments representing the generatrices of a cylinder. Generates a specified number of line segments along the surface of the cylinder, each representing a generatrix. :param number_lines: The number of generatrices to generate. Default is 30 :type number_lines: int :param length: The length of the cylinder along the z-direction. Default is 1. :type length: float :return: A list of LineSegment3D instances representing the generatrices of the cylinder. :rtype: List[LineSegment3D] """ list_generatrices = [] for i in range(number_lines): theta = i / (number_lines - 1) * design3d.TWO_PI start = self.point2d_to_3d(design3d.Point2D(theta, -0.5 * length)) end = self.point2d_to_3d(design3d.Point2D(theta, 0.5 * length)) generatrix = edges.LineSegment3D(start, end) list_generatrices.append(generatrix) return list_generatrices
[docs] def get_circle_generatrices(self, number_circles: int = 10, length: float = 1.0): """ Retrieve circles representing the generatrices of a cylinder. Generates a specified number of circles along the surface of the cylinder, each representing a generatrix. :param number_circles: The number of generatrices to generate. Default is 10 :type number_circles: int :param length: The length of the cylinder along the z-direction. Default is 1. :type length: float :return: A list of Circle3D instances representing the generatrices of the cylinder. :rtype: List[Circle3D] """ return [self.v_iso((-0.5 + j / (number_circles - 1)) * length) for j in range(number_circles)]
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), length=None, **kwargs): """ Plot the cylindrical surface in the local frame normal direction. :param ax: Matplotlib Axes3D object to plot on. If None, create a new figure. :type ax: Axes3D or None :param edge_style: edge styles. :type edge_style: EdgeStyle. :param length: plotted length :type length: float :return: Matplotlib Axes3D object containing the plotted wire-frame. :rtype: Axes3D """ ncircles = 50 nlines = 50 if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') if length is None: length = self.radius self.frame.plot(ax=ax, color=edge_style.color, ratio=self.radius) for edge in self.get_generatrices(nlines, length): edge.plot(ax=ax, edge_style=edge_style) circles = self.get_circle_generatrices(ncircles, length) for circle in circles: circle.plot(ax=ax, edge_style=edge_style) return ax
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Coverts a parametric coordinate on the surface into a 3D spatial point (x, y, z). :param point2d: Point at the ToroidalSuface3D :type point2d: `design3d.`Point2D` """ point = design3d.Point3D(self.radius * math.cos(point2d.x), self.radius * math.sin(point2d.x), point2d.y) return self.frame.local_to_global_coordinates(point)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the cylindrical surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the cylindrical surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the cylindrical surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ center = np.asarray(self.frame.origin) x = np.asarray([self.frame.u[0], self.frame.u[1], self.frame.u[2]]) y = np.asarray([self.frame.v[0], self.frame.v[1], self.frame.v[2]]) z = np.asarray([self.frame.w[0], self.frame.w[1], self.frame.w[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] v_values = points[:, 1] x_component = np.cos(u_values) * x y_component = np.sin(u_values) * y z_component = v_values * z return center + self.radius * (x_component + y_component) + z_component
[docs] def point3d_to_2d(self, point3d): """ Returns the cylindrical coordinates design3d.Point2D(theta, z) of a Cartesian coordinates point (x, y, z). :param point3d: Point at the CylindricalSuface3D :type point3d: `design3d.`Point3D` """ x, y, z = self.frame.global_to_local_coordinates(point3d) # Do not delete this, mathematical problem when x and y close to zero but not 0 if abs(x) < 1e-12: x = 0 if abs(y) < 1e-12: y = 0 theta = math.atan2(y, x) if abs(theta) < 1e-9: theta = 0.0 return design3d.Point2D(theta, z)
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a CylindricalSurface3D. :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 CylindricalSurface3D object. :rtype: :class:`design3d.faces.CylindricalSurface3D` """ length_conversion_factor = kwargs.get("length_conversion_factor", 1) frame = object_dict[arguments[1]] radius = float(arguments[2]) * length_conversion_factor return cls(frame, radius, arguments[0][1:-1])
[docs] def to_step(self, current_id): """ Converts the object to a STEP representation. :param current_id: The ID of the last written primitive. :type current_id: int :return: The STEP representation of the object and the last ID. :rtype: tuple[str, list[int]] """ content, frame_id = self.frame.to_step(current_id) current_id = frame_id + 1 content += f"#{current_id} = CYLINDRICAL_SURFACE('{self.name}',#{frame_id},{round(1000 * self.radius, 4)});\n" return content, [current_id]
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new CylindricalSurface3D. :param side: 'old' or 'new' """ new_frame = self.frame.frame_mapping(frame, side) return CylindricalSurface3D(new_frame, self.radius, name=self.name)
[docs] def rectangular_cut(self, theta1: float, theta2: float, param_z1: float, param_z2: float, name: str = ''): """Deprecated method, Use CylindricalFace3D from_surface_rectangular_cut method.""" raise AttributeError('Use CylindricalFace3D from_surface_rectangular_cut method')
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ CylindricalFace3D rotation. :param center: rotation center. :param axis: rotation axis. :param angle: angle rotation. :return: a new rotated Plane3D. """ new_frame = self.frame.rotation(center=center, axis=axis, angle=angle) return CylindricalSurface3D(new_frame, self.radius)
[docs] def translation(self, offset: design3d.Vector3D): """ CylindricalFace3D translation. :param offset: translation vector. :return: A new translated CylindricalFace3D. """ return CylindricalSurface3D(self.frame.translation(offset), self.radius)
[docs] def grid3d(self, grid2d: grid.Grid2D): """ Generate 3d grid points of a Cylindrical surface, based on a Grid2D. """ points_2d = grid2d.points points_3d = [self.point2d_to_3d(point2d) for point2d in points_2d] return points_3d
[docs] def line_intersections(self, line: curves.Line3D): """Gets intersections between a line and a Cylindrical Surface 3D.""" line_2d = line.to_2d(self.frame.origin, self.frame.u, self.frame.v) if line_2d is None: return [] origin2d = self.frame.origin.to_2d(self.frame.origin, self.frame.u, self.frame.v) distance_line2d_to_origin = line_2d.point_distance(origin2d) if distance_line2d_to_origin > self.radius: return [] a_prime = line_2d.point1 b_prime = line_2d.point2 a_prime_minus_b_prime = a_prime - b_prime t_param = a_prime.dot(a_prime_minus_b_prime) / a_prime_minus_b_prime.dot(a_prime_minus_b_prime) k_param = math.sqrt( (self.radius ** 2 - distance_line2d_to_origin ** 2) / a_prime_minus_b_prime.dot(a_prime_minus_b_prime)) intersection1 = line.point1 + (t_param + k_param) * (line.direction_vector()) intersection2 = line.point1 + (t_param - k_param) * (line.direction_vector()) if intersection1 == intersection2: return [intersection1] return [intersection1, intersection2]
[docs] def parallel_plane_intersection(self, plane3d): """ Cylinder plane intersections when plane's normal is perpendicular with the cylinder axis. :param plane3d: intersecting plane :return: list of intersecting curves """ distance_plane_cylinder_axis = plane3d.point_distance(self.frame.origin) if distance_plane_cylinder_axis > self.radius: return [] if math.isclose(self.frame.w.dot(plane3d.frame.u), 0, abs_tol=1e-6): line = curves.Line3D(plane3d.frame.origin, plane3d.frame.origin + plane3d.frame.u) else: line = curves.Line3D(plane3d.frame.origin, plane3d.frame.origin + plane3d.frame.v) line_intersections = self.line_intersections(line) lines = [] for intersection in line_intersections: lines.append(curves.Line3D(intersection, intersection + self.frame.w)) return lines
[docs] def perpendicular_plane_intersection(self, plane3d): """ Cylinder plane intersections when plane's normal is parallel with the cylinder axis. :param plane3d: intersecting plane :return: list of intersecting curves """ line = curves.Line3D(self.frame.origin, self.frame.origin + self.frame.w) center3d_plane = plane3d.line_intersections(line)[0] circle3d = curves.Circle3D(design3d.Frame3D(center3d_plane, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), self.radius) return [circle3d]
[docs] def concurrent_plane_intersection(self, plane3d: Plane3D): """ Cylindrical plane intersections when plane's normal is concurrent with the cone's axis, but not orthogonal. :param plane3d: intersecting plane. :return: list of intersecting curves. """ plane_normal = self.frame.w.cross(plane3d.frame.w) plane2 = Plane3D.from_normal(self.frame.origin, plane_normal) plane2_plane3d_intersections = plane3d.plane_intersections(plane2) line_intersections = self.line_intersections(plane2_plane3d_intersections[0]) if not line_intersections: return [] ellipse_center = (line_intersections[0] + line_intersections[1]) / 2 line2 = curves.Line3D.from_point_and_vector(ellipse_center, plane_normal) line_intersections2 = self.line_intersections(line2) major_dir = (line_intersections[0] - ellipse_center).unit_vector() major_axis = ellipse_center.point_distance(line_intersections[0]) minor_dir = (line_intersections2[0] - ellipse_center).unit_vector() minor_axis = ellipse_center.point_distance(line_intersections2[0]) if minor_axis > major_axis: major_axis, minor_axis = minor_axis, major_axis major_dir, minor_dir = minor_dir, major_dir ellipse = curves.Ellipse3D(major_axis, minor_axis, design3d.Frame3D(ellipse_center, major_dir, minor_dir, plane3d.frame.w)) return [ellipse]
[docs] def plane_intersections(self, plane3d): """ Cylinder intersections with a plane. :param plane3d: intersecting plane. :return: list of intersecting curves. """ if math.isclose(abs(plane3d.frame.w.dot(self.frame.w)), 0, abs_tol=1e-6): return self.parallel_plane_intersection(plane3d) if math.isclose(abs(plane3d.frame.w.dot(self.frame.w)), 1, abs_tol=1e-6): return self.perpendicular_plane_intersection(plane3d) return self.concurrent_plane_intersection(plane3d)
[docs] def conicalsurface_intersections(self, conical_surface: 'ConicalSurface3D'): """ Cylinder Surface intersections with a Conical surface. :param conical_surface: intersecting plane. :return: list of intersecting curves. """ def _list_generatrices_intersections(surface, other_surface): linesegments = other_surface.get_generatrices(50, 2) all_generatrices_intersecting = True lists_intersections = [[], []] for generatrix in linesegments: linseg_intersections = surface.line_intersections(generatrix.line) if not linseg_intersections: all_generatrices_intersecting = False for index, point in enumerate(linseg_intersections): if other_surface.point_distance(point) < 1e-6 and \ not point.in_list(lists_intersections[index]): lists_intersections[index].append(point) return lists_intersections, all_generatrices_intersecting cone_generatrices_point_intersections, all_cone_generatrices_intersecting_cylinder = \ _list_generatrices_intersections(self, conical_surface) cylinder_generatrices_point_intersections, all_cylinder_generatrices_intersecting_cone = \ _list_generatrices_intersections(conical_surface, self) if all_cylinder_generatrices_intersecting_cone: intersections_points = cylinder_generatrices_point_intersections elif all_cone_generatrices_intersecting_cylinder: intersections_points = cone_generatrices_point_intersections if not cone_generatrices_point_intersections[1]: intersections_points = [[]] for point in ( cylinder_generatrices_point_intersections[0] + cylinder_generatrices_point_intersections[1] + cone_generatrices_point_intersections[0] + cone_generatrices_point_intersections[1]): if not point.in_list(intersections_points[0]): intersections_points[0].append(point) elif not all_cone_generatrices_intersecting_cylinder: intersections_points = [[]] for point in (cylinder_generatrices_point_intersections[0] + cylinder_generatrices_point_intersections[1] + cone_generatrices_point_intersections[0] + cone_generatrices_point_intersections[1]): if not point.in_list(intersections_points[0]): intersections_points[0].append(point) list_curves = [] for list_points in intersections_points: order_ed_points = d3d_common_operations.order_points_list_for_nearest_neighbor(list_points) bspline = edges.BSplineCurve3D.from_points_interpolation(order_ed_points + [order_ed_points[0]], 4, centripetal=False) list_curves.append(bspline) return list_curves
[docs] def is_coincident(self, surface3d, abs_tol: float = 1e-6): """ Verifies if two CylindricalSurfaces are coincident. :param surface3d: surface to verify. :param abs_tol: tolerance. :return: True if they are coincident, False otherwise. """ if not isinstance(self, surface3d.__class__): return False line = curves.Line3D.from_point_and_vector(surface3d.frame.origin, surface3d.frame.w) distance_to_self_origin = line.point_distance(self.frame.origin) if math.isclose(abs(self.frame.w.dot(surface3d.frame.w)), 1.0, abs_tol=abs_tol) and \ math.isclose(distance_to_self_origin, 0.0, abs_tol=abs_tol) and self.radius == surface3d.radius: return True return False
[docs] def point_belongs(self, point3d, abs_tol: float = 1e-5): """ Verifies if a given point is on the CylindricalSurface3D. :param point3d: Point to verify. :param abs_tol: Tolerance. :return: True if point on surface, False otherwise. """ new_point = self.frame.global_to_local_coordinates(point3d) if math.isclose(new_point.x ** 2 + new_point.y ** 2, self.radius ** 2, abs_tol=abs_tol): return True return False
def _sphere_cylinder_tangent_intersections(self, frame, distance_axis_sphere_center): """ Gets the intersections between a sphere tangent to the cylinder. :param frame: frame for local calculations. Frame is such that w is the cylinder axis, and u passes through the sphere's center. :param distance_axis_sphere_center: distance of sphere's center to cylinder axis. :return: return a list with the intersecting curves. """ curves_ = [] for phi_range in [(0, math.pi), (math.pi, 2 * math.pi), (2 * math.pi, 3 * math.pi), (3 * math.pi, 4 * math.pi)]: phi = np.linspace(phi_range[0], phi_range[1], 100) intersection_points = [design3d.Point3D(x_comp, y_comp, z_comp) for x_comp, y_comp, z_comp in zip( self.radius * np.cos(phi), self.radius * np.sin(phi), 2 * math.sqrt(distance_axis_sphere_center * self.radius) * np.cos(phi / 2))] bspline = edges.BSplineCurve3D.from_points_interpolation(intersection_points, 4, centripetal=False) curves_.append(bspline) global_intersections = [edge.frame_mapping(frame, 'old') for edge in curves_] return global_intersections def _helper_spherical_intersections_points(self, spherical_surface, distance_axis_sphere_center): """ Helper method to get spherical intersections points. :param spherical_surface: spherical surface. :param distance_axis_sphere_center: distance cylinder axis to sphere center. :return: intersection points. """ b = (spherical_surface.radius ** 2 - self.radius ** 2 - distance_axis_sphere_center ** 2) / (2 * distance_axis_sphere_center) if spherical_surface.radius > self.radius + distance_axis_sphere_center: phi_0, phi_1, two_curves = 0, 2 * math.pi, True else: phi_0 = math.acos(-b / self.radius) phi_1 = phi_0 - 0.000001 phi_0 = -phi_0 + 0.000001 two_curves = False phi = np.linspace(phi_0, phi_1, 400) x_components = self.radius * np.cos(phi) y_components = self.radius * np.sin(phi) z_components1 = np.sqrt(2 * distance_axis_sphere_center * (b + x_components)) inters_points = [[design3d.Point3D(x_comp, y_comp, z_comp) for x_comp, y_comp, z_comp in zip(x_components, y_components, z_components1)], [design3d.Point3D(x_comp, y_comp, -z_comp) for x_comp, y_comp, z_comp in zip(x_components, y_components, z_components1)]] if not two_curves: inters_points = d3d_common_operations.separate_points_by_closeness(inters_points[0] + inters_points[1]) return inters_points
[docs] def sphericalsurface_intersections(self, spherical_surface: 'SphericalSurface3D'): """ Cylinder Surface intersections with a Spherical surface. :param spherical_surface: intersecting sphere. :return: list of intersecting curves. """ line_axis = curves.Line3D.from_point_and_vector(self.frame.origin, self.frame.w) distance_axis_sphere_center = line_axis.point_distance(spherical_surface.frame.origin) if distance_axis_sphere_center < self.radius: if distance_axis_sphere_center + spherical_surface.radius < self.radius: return [] if math.isclose(distance_axis_sphere_center, 0.0, abs_tol=1e-6): if math.isclose(self.radius, spherical_surface.radius): return [spherical_surface.get_circle_at_z(0)] z_plane_position = math.sqrt(spherical_surface.radius ** 2 - self.radius ** 2) circle1 = spherical_surface.get_circle_at_z(z_plane_position) circle2 = spherical_surface.get_circle_at_z(-z_plane_position) return [circle1, circle2] if distance_axis_sphere_center - spherical_surface.radius > self.radius: return [] point_projection, _ = line_axis.point_projection(spherical_surface.frame.origin) vector = (spherical_surface.frame.origin - point_projection).unit_vector() frame = design3d.Frame3D(point_projection, vector, self.frame.w.cross(vector), self.frame.w) if math.isclose(distance_axis_sphere_center + self.radius, spherical_surface.radius, abs_tol=1e-6): return self._sphere_cylinder_tangent_intersections(frame, distance_axis_sphere_center) inters_points = self._helper_spherical_intersections_points(spherical_surface, distance_axis_sphere_center) curves_ = [edges.BSplineCurve3D.from_points_interpolation(points, 4, centripetal=False) for points in inters_points] return [edge.frame_mapping(frame, 'old') for edge in curves_]
def _cylindrical_intersection_points(self, cylindricalsurface: 'SphericalSurface3D'): """ Gets the points of intersections between two cylindrical surfaces. :param cylindricalsurface: other Cylindrical surface 3d. :return: points of intersections. """ cyl_generatrices = self.get_generatrices(200, self.radius * 10) + \ self.get_circle_generatrices(200, self.radius * 10) intersection_points = [] for gene in cyl_generatrices: intersections = cylindricalsurface.edge_intersections(gene) for intersection in intersections: if not design3d.core.point_in_list(intersection, intersection_points): intersection_points.append(intersection) return intersection_points
[docs] def cylindricalsurface_intersections(self, cylindricalsurface: 'CylindricalSurface3D'): """ Gets intersections between two cylindrical surfaces 3d. :param cylindricalsurface: other cylindrical surface. :return: a list containing the resulting intersections, if there are any. """ curves_ = [] if self.frame.w.is_colinear_to(cylindricalsurface.frame.w): circle1 = curves.Circle3D(self.frame, self.radius).to_2d(self.frame.origin, self.frame.u, self.frame.v) circle2 = curves.Circle3D(cylindricalsurface.frame, cylindricalsurface.radius).to_2d( self.frame.origin, self.frame.u, self.frame.v) circle2d_intersections = circle1.circle_intersections(circle2) for point in circle2d_intersections: point3d = point.to_3d(self.frame.origin, self.frame.u, self.frame.v) curves_.append(curves.Line3D.from_point_and_vector(point3d, self.frame.w)) return curves_ intersection_points = self._cylindrical_intersection_points(cylindricalsurface) if not intersection_points: return [] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 4, centripetal=False) curves_.append(bspline) return curves_
[docs] def u_iso(self, u: float) -> curves.Line3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A line 3D :rtype: :class:`curves.Line3D` """ point_at_u = self.point2d_to_3d(design3d.Point2D(u, 0.0)) return curves.Line3D.from_point_and_vector(point_at_u, self.frame.w)
[docs] def v_iso(self, v: float) -> curves.Circle3D: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A Circle 3D :rtype: :class:`curves.Circle3D` """ frame = self.frame.translation(self.frame.w * v) return curves.Circle3D(frame, self.radius)
[docs] def normal_at_point(self, point: design3d.Point3D): """ Gets normal vector at given point on the surface. :param point: point to be verified. :return: normal """ if not self.point_belongs(point): raise ValueError('Point given not on surface.') theta, _ = self.point3d_to_2d(point) normal = math.cos(theta) * self.frame.u + math.sin(theta) * self.frame.v return normal
[docs] class ToroidalSurface3D(UVPeriodicalSurface): """ The local plane is defined by (theta, phi). Theta is the angle around the big (R) circle and phi around the small (r). :param frame: Tore's frame: origin is the center, u is pointing at theta=0. :param major_radius: Tore's radius. :param r: Circle to revolute radius. See Also Definitions of R and r according to https://en.wikipedia.org/wiki/Torus. """ face_class = 'ToroidalFace3D' x_periodicity = design3d.TWO_PI y_periodicity = design3d.TWO_PI def __init__(self, frame: design3d.Frame3D, major_radius: float, minor_radius: float, name: str = ''): self.major_radius = major_radius self.minor_radius = minor_radius UVPeriodicalSurface.__init__(self, frame=frame, name=name) self._bbox = None def __hash__(self): return hash((self.__class__.__name__, self.frame, self.major_radius, self.minor_radius)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.frame == other.frame and \ self.major_radius == other.major_radius and \ self.minor_radius == other.minor_radius: return True return False @property def u_domain(self): """The parametric domain of the surface in the U direction.""" return -math.pi, math.pi @property def v_domain(self): """The parametric domain of the surface in the V direction.""" return -math.pi, math.pi @cached_property def outer_radius(self): """Get torus outer radius.""" return self.major_radius + self.minor_radius @cached_property def inner_radius(self): """Get torus inner radius.""" return self.major_radius - self.minor_radius
[docs] def torus_arcs(self, number_arcs: int = 50): """ Retrieve torus arcs representing the generatrices of a Torus. :param number_arcs: The number of generatrices to generate. Default is 30 :type number_arcs: int :return: A list of Circle3D instances representing the generatrices of the torus. :rtype: List[Circle3D] """ return [self.u_iso(i / number_arcs * design3d.TWO_PI) for i in range(number_arcs)]
def _torus_circle_generatrices_xy(self, number_arcs: int = 50): """ Retrieve circle generatrices in cutting planes parallel to the XY plane of the torus local system. :param number_arcs: The number of generatrices to generate. Default is 50. :type number_arcs: int :return: A list of Circle3D instances representing the generatrices in the XY plane. :rtype: List[Circle3D] """ initial_point = self.frame.origin circles = [] phis = np.linspace(-0.5 * math.pi, 0.5 * math.pi, number_arcs) z_positions = self.minor_radius * np.sin(phis) r_cossines = self.minor_radius * np.cos(phis) radiuses1 = self.major_radius - r_cossines radiuses2 = self.major_radius + r_cossines for i, radius1, radius2 in zip(z_positions, radiuses1, radiuses2): i_center = initial_point.translation(self.frame.w * i) frame = design3d.Frame3D(i_center, self.frame.u, self.frame.v, self.frame.w) circles.append(curves.Circle3D(frame, radius1)) if radius1 == radius2: continue circles.append(curves.Circle3D(frame, radius2)) return circles
[docs] @classmethod def dict_to_object(cls, dict_, **kwargs) -> 'ToroidalSurface3D': """Creates a ToroidalSurface3D from a dictionary.""" frame = design3d.Frame3D.dict_to_object(dict_['frame']) name = dict_['name'] if 'tore_radius' in dict_: # fix done 26/10/2023 major_radius = dict_['tore_radius'] minor_radius = dict_['small_radius'] else: major_radius = dict_['major_radius'] minor_radius = dict_['minor_radius'] return cls(frame, major_radius, minor_radius, name)
@property def bounding_box(self): """ Returns the surface bounding box. """ if not self._bbox: self._bbox = self._bounding_box() return self._bbox def _bounding_box(self): """ Calculates the BoundingBox for the complete Toroidal Surface 3D. :return: surface bounding box. """ distance = self.major_radius + self.minor_radius point1 = self.frame.origin + \ self.frame.u * distance + self.frame.v * distance + self.frame.w * self.minor_radius point2 = self.frame.origin + \ self.frame.u * distance + self.frame.v * distance - self.frame.w * self.minor_radius point3 = self.frame.origin + \ self.frame.u * distance - self.frame.v * distance + self.frame.w * self.minor_radius point4 = self.frame.origin + \ self.frame.u * distance - self.frame.v * distance - self.frame.w * self.minor_radius point5 = self.frame.origin - \ self.frame.u * distance + self.frame.v * distance + self.frame.w * self.minor_radius point6 = self.frame.origin - \ self.frame.u * distance + self.frame.v * distance - self.frame.w * self.minor_radius point7 = self.frame.origin - \ self.frame.u * distance - self.frame.v * distance + self.frame.w * self.minor_radius point8 = self.frame.origin - \ self.frame.u * distance - self.frame.v * distance - self.frame.w * self.minor_radius return design3d.core.BoundingBox.from_points( [point1, point2, point3, point4, point5, point6, point7, point8])
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Coverts a parametric coordinate on the surface into a 3D spatial point (x, y, z). :param point2d: Point at the ToroidalSuface3D :type point2d: `design3d.`Point2D` """ theta, phi = point2d x = (self.major_radius + self.minor_radius * math.cos(phi)) * math.cos(theta) y = (self.major_radius + self.minor_radius * math.cos(phi)) * math.sin(theta) z = self.minor_radius * math.sin(phi) return self.frame.local_to_global_coordinates(design3d.Point3D(x, y, z))
[docs] def point3d_to_2d(self, point3d): """ Transform a 3D spatial point (x, y, z) into a 2D spherical parametric point (theta, phi). """ x, y, z = self.frame.global_to_local_coordinates(point3d) z = min(self.minor_radius, max(-self.minor_radius, z)) # Do not delete this, mathematical problem when x and y close to zero (should be zero) but not 0 # Generally this is related to uncertainty of step files. if abs(x) < 1e-12: x = 0.0 if abs(y) < 1e-12: y = 0.0 if abs(z) < 1e-6: z = 0.0 z_r = z / self.minor_radius phi = math.asin(z_r) if abs(phi) < 1e-9: phi = 0 u = self.major_radius + math.sqrt((self.minor_radius ** 2) - (z ** 2)) u1, u2 = x / u, y / u theta = math.atan2(u2, u1) vector_to_tube_center = design3d.Vector3D(abs(self.major_radius) * math.cos(theta), abs(self.major_radius) * math.sin(theta), 0) vector_from_tube_center_to_point = design3d.Vector3D(x, y, z) - vector_to_tube_center phi2 = design3d.geometry.vectors3d_angle(vector_to_tube_center, vector_from_tube_center_to_point) if phi >= 0 and phi2 > 0.5 * math.pi: phi = math.pi - phi elif phi < 0 and phi2 > 0.5 * math.pi: phi = -math.pi - phi if abs(theta) < 1e-9: theta = 0.0 if abs(phi) < 1e-9: phi = 0.0 if self.major_radius < self.minor_radius: phi_self_intersection = math.acos(-self.major_radius / self.minor_radius) if abs(phi) > phi_self_intersection: if theta >= 0.0: theta -= math.pi else: theta += math.pi return design3d.Point2D(theta, phi)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the toroidal surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the toroidal surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the toroidal surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ center = np.asarray(self.frame.origin) x = np.asarray([self.frame.u[0], self.frame.u[1], self.frame.u[2]]) y = np.asarray([self.frame.v[0], self.frame.v[1], self.frame.v[2]]) z = np.asarray([self.frame.w[0], self.frame.w[1], self.frame.w[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] v_values = points[:, 1] common_term = self.major_radius + self.minor_radius * np.cos(v_values) x_component = np.cos(u_values) * x y_component = np.sin(u_values) * y z_component = self.minor_radius * np.sin(v_values) * z return center + common_term * (x_component + y_component) + z_component
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a ToroidalSurface3D. :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 ToroidalSurface3D object. :rtype: :class:`design3d.faces.ToroidalSurface3D` """ length_conversion_factor = kwargs.get("length_conversion_factor", 1) frame = object_dict[arguments[1]] rcenter = abs(float(arguments[2])) * length_conversion_factor rcircle = abs(float(arguments[3])) * length_conversion_factor return cls(frame, rcenter, rcircle, arguments[0][1:-1])
[docs] def to_step(self, current_id): """ Converts the object to a STEP representation. :param current_id: The ID of the last written primitive. :type current_id: int :return: The STEP representation of the object and the last ID. :rtype: tuple[str, list[int]] """ content, frame_id = self.frame.to_step(current_id) current_id = frame_id + 1 content += f"#{current_id} = TOROIDAL_SURFACE('{self.name}',#{frame_id}," \ f"{round(1000 * self.major_radius, 4)},{round(1000 * self.minor_radius, 4)});\n" return content, [current_id]
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new ToroidalSurface3D. :param frame: The new frame to map to. :type frame: `design3d.Frame3D :param side: Indicates whether the frame should be mapped to the 'old' or 'new' frame. Acceptable values are 'old' or 'new'. :type side: str """ new_frame = self.frame.frame_mapping(frame, side) return ToroidalSurface3D(new_frame, self.major_radius, self.minor_radius, name=self.name)
[docs] def rectangular_cut(self, theta1: float, theta2: float, phi1: float, phi2: float, name: str = ""): """Deprecated method, Use ToroidalFace3D from_surface_rectangular_cut method.""" raise AttributeError('Use ToroidalFace3D from_surface_rectangular_cut method')
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Converts the parametric boundary representation into a 3D primitive. """ n = len(bspline_curve2d.control_points) points = [self.point2d_to_3d(p) for p in bspline_curve2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, bspline_curve2d.degree, centripetal=True)]
def _helper_arc3d_to_2d_periodicity_verifications(self, arc3d, start, end): """ Verifies if arc 3D contains discontinuity and undefined start/end points on parametric domain. """ point_theta_discontinuity = self.point2d_to_3d(design3d.Point2D(math.pi, start.y)) theta_discontinuity = (arc3d.point_belongs(point_theta_discontinuity) and not arc3d.is_point_edge_extremity(point_theta_discontinuity) and not self.frame.w.is_perpendicular_to(arc3d.frame.w)) point_phi_discontinuity = self.point2d_to_3d(design3d.Point2D(start.x, math.pi)) phi_discontinuity = (arc3d.point_belongs(point_phi_discontinuity) and not arc3d.is_point_edge_extremity(point_phi_discontinuity) and not self.frame.w.is_colinear_to(arc3d.frame.w)) undefined_start_theta = arc3d.start.is_close(point_theta_discontinuity) or abs(start.x) == math.pi undefined_end_theta = arc3d.end.is_close(point_theta_discontinuity) or abs(end.x) == math.pi undefined_start_phi = arc3d.start.is_close(point_phi_discontinuity) or start.y == math.pi undefined_end_phi = arc3d.end.is_close(point_phi_discontinuity) or end.y == math.pi return theta_discontinuity, phi_discontinuity, undefined_start_theta, undefined_end_theta, \ undefined_start_phi, undefined_end_phi
[docs] def fullarc3d_to_2d(self, fullarc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(fullarc3d.start) end = self.point3d_to_2d(fullarc3d.end) point_after_start, point_before_end = self._reference_points(fullarc3d) theta_discontinuity, phi_discontinuity, undefined_start_theta, undefined_end_theta, \ undefined_start_phi, undefined_end_phi = self._helper_arc3d_to_2d_periodicity_verifications( fullarc3d, start, end) start, end = d3d_parametric.arc3d_to_toroidal_coordinates_verification( [start, end], [undefined_start_theta, undefined_end_theta, undefined_start_phi, undefined_end_phi], [point_after_start, point_before_end], [theta_discontinuity, phi_discontinuity]) theta1, phi1 = start theta3, phi3 = point_after_start if self.frame.w.is_colinear_to(fullarc3d.circle.normal, abs_tol=1e-4): if theta1 > theta3: end = design3d.Point2D(theta1 - design3d.TWO_PI, phi1) elif theta1 < theta3: end = design3d.Point2D(theta1 + design3d.TWO_PI, phi1) return [edges.LineSegment2D(start, end)] if phi1 > phi3: end = design3d.Point2D(theta1, phi1 - design3d.TWO_PI) elif phi1 < phi3: end = design3d.Point2D(theta1, phi1 + design3d.TWO_PI) return [edges.LineSegment2D(start, end)]
[docs] def arc3d_to_2d(self, arc3d): """ Converts the arc from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(arc3d.start) end = self.point3d_to_2d(arc3d.end) point_after_start, point_before_end = self._reference_points(arc3d) theta_discontinuity, phi_discontinuity, undefined_start_theta, undefined_end_theta, \ undefined_start_phi, undefined_end_phi = self._helper_arc3d_to_2d_periodicity_verifications(arc3d, start, end) start, end = d3d_parametric.arc3d_to_toroidal_coordinates_verification( [start, end], [undefined_start_theta, undefined_end_theta, undefined_start_phi, undefined_end_phi], [point_after_start, point_before_end], [theta_discontinuity, phi_discontinuity]) return [edges.LineSegment2D(start, end)]
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ point_after_start, point_before_end = self._reference_points(bspline_curve3d) theta3, phi3 = point_after_start theta4, phi4 = point_before_end n = bspline_curve3d.ctrlpts.shape[0] points3d = bspline_curve3d.discretization_points(number_points=n) points = [self.point3d_to_2d(p) for p in points3d] theta1, phi1 = points[0] theta2, phi2 = points[-1] # Verify if theta1 or theta2 point should be -pi because atan2() -> ]-pi, pi] if abs(theta1) == math.pi: theta1 = repair_start_end_angle_periodicity(theta1, theta3) if abs(theta2) == math.pi: theta2 = repair_start_end_angle_periodicity(theta2, theta4) # Verify if phi1 or phi2 point should be -pi because phi -> ]-pi, pi] if abs(phi1) == math.pi: phi1 = repair_start_end_angle_periodicity(phi1, phi3) if abs(phi2) == math.pi: phi2 = repair_start_end_angle_periodicity(phi2, phi4) points[0] = design3d.Point2D(theta1, phi1) points[-1] = design3d.Point2D(theta2, phi2) theta_list = [point.x for point in points] phi_list = [point.y for point in points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) phi_discontinuity, indexes_phi_discontinuity = angle_discontinuity(phi_list) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") if phi_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_phi_discontinuity, "y") points = verify_repeated_parametric_points(points) return [edges.BSplineCurve2D.from_points_interpolation(points, bspline_curve3d.degree)]
[docs] def triangulation(self): """ Triangulation. :rtype: display.Mesh3D """ face = self.rectangular_cut(0, design3d.TWO_PI, 0, design3d.TWO_PI) return face.triangulation()
[docs] def translation(self, offset: design3d.Vector3D): """ ToroidalSurface3D translation. :param offset: translation vector :return: A new translated ToroidalSurface3D """ return ToroidalSurface3D(self.frame.translation( offset), self.major_radius, self.minor_radius)
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ ToroidalSurface3D rotation. :param center: rotation center. :param axis: rotation axis. :param angle: angle rotation. :return: a new rotated ToroidalSurface3D. """ new_frame = self.frame.rotation(center=center, axis=axis, angle=angle) return self.__class__(new_frame, self.major_radius, self.minor_radius)
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), **kwargs): """Plot torus arcs.""" if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') self.frame.plot(ax=ax, ratio=self.major_radius) circles = self.torus_arcs(100) + self._torus_circle_generatrices_xy(30) for circle in circles: circle.plot(ax=ax, edge_style=edge_style) return ax
[docs] def point_projection(self, point3d): """ Returns the projection of the point on the toroidal surface. :param point3d: Point to project. :type point3d: design3d.Point3D :return: A point on the surface :rtype: design3d.Point3D """ x, y, z = self.frame.global_to_local_coordinates(point3d) if abs(x) < 1e-12: x = 0 if abs(y) < 1e-12: y = 0 theta = math.atan2(y, x) vector_to_tube_center = design3d.Vector3D(self.major_radius * math.cos(theta), self.major_radius * math.sin(theta), 0) vector_from_tube_center_to_point = design3d.Vector3D(x, y, z) - vector_to_tube_center phi = design3d.geometry.vectors3d_angle(vector_to_tube_center, vector_from_tube_center_to_point) if z < 0: phi = 2 * math.pi - phi if abs(theta) < 1e-9: theta = 0.0 if abs(phi) < 1e-9: phi = 0.0 return self.point2d_to_3d(design3d.Point2D(theta, phi))
def _reference_points(self, edge): """ Helper function to return points of reference on the edge to fix some parametric periodical discontinuities. """ length = edge.length() point_after_start = self.point3d_to_2d(edge.point_at_abscissa(0.01 * length)) point_before_end = self.point3d_to_2d(edge.point_at_abscissa(0.98 * length)) theta3, phi3 = point_after_start theta4, phi4 = point_before_end if abs(theta3) == math.pi or abs(theta3) == 0.5 * math.pi or \ abs(phi3) == math.pi or abs(phi3) == 0.5 * math.pi: point_after_start = self.point3d_to_2d(edge.point_at_abscissa(0.02 * length)) if abs(theta4) == math.pi or abs(theta4) == 0.5 * math.pi or \ abs(phi4) == math.pi or abs(phi4) == 0.5 * math.pi: point_before_end = self.point3d_to_2d(edge.point_at_abscissa(0.97 * length)) return point_after_start, point_before_end def _get_line_intersections_solution_roots(self, line): """ Line intersections helper: get roots. :param line: other line. :return: roots. """ vector = line.unit_direction_vector() coeff_a = vector.x ** 2 + vector.y ** 2 + vector.z ** 2 coeff_b = 2 * (line.point1.x * vector.x + line.point1.y * vector.y + line.point1.z * vector.z) coeff_c = (line.point1.x ** 2 + line.point1.y ** 2 + line.point1.z ** 2 + self.major_radius ** 2 - self.minor_radius ** 2) coeff_d = vector.x ** 2 + vector.y ** 2 coeff_e = 2 * (line.point1.x * vector.x + line.point1.y * vector.y) coeff_f = line.point1.x ** 2 + line.point1.y ** 2 solutions = np.roots([(coeff_a ** 2), 2 * coeff_a * coeff_b, (2 * coeff_a * coeff_c + coeff_b ** 2 - 4 * coeff_d * self.major_radius ** 2), (2 * coeff_b * coeff_c - 4 * self.major_radius ** 2 * coeff_e), coeff_c ** 2 - 4 * self.major_radius ** 2 * coeff_f]) return solutions
[docs] def line_intersections(self, line: curves.Line3D): """ Calculates the intersections between the toroidal surface and an infinite line. :param line: other line. :return: intersections. """ if not self.frame.origin.is_close(design3d.O3D) or not self.frame.w.is_close(design3d.Z3D): frame_mapped_surface = self.frame_mapping(self.frame, 'new') frame_mapped_line = line.frame_mapping(self.frame, 'new') local_intersections = frame_mapped_surface.line_intersections(frame_mapped_line) global_intersections = [self.frame.local_to_global_coordinates(point) for point in local_intersections] return global_intersections vector = line.unit_direction_vector() solutions = self._get_line_intersections_solution_roots(line) intersections = [] for sol_param in sorted(solutions): if isinstance(sol_param, np.complex128): if sol_param.imag == 0.0: intersections.append(line.point1 + sol_param.real * vector) else: intersections.append(line.point1 + sol_param * vector) return intersections
[docs] def circle_intersections(self, circle: curves.Circle3D): """ Calculates the intersections between a toroidal surface 3d and a Circle 3D. :param circle: other circle to verify intersections. :return: a list of intersection points, if there exists any. """ toroidal_plane = Plane3D(self.frame) if toroidal_plane.point_distance(circle.center) >= circle.radius + self.minor_radius: return [] circle2_ = curves.Circle3D(self.frame, self.major_radius) circle_distance = circle.circle_distance(circle2_, False) if circle_distance > self.minor_radius: return [] return self.curve_intersections(circle)
def _helper_parallel_plane_intersections_through_origin(self, plane3d): """ Helper method to get intersection between torus and plane through the origin. :param plane3d: other plane. :return: two circles. """ plane1 = Plane3D(self.frame) plane_intersections = plane1.plane_intersections(plane3d) center1 = self.frame.origin + plane_intersections[0].unit_direction_vector() * self.major_radius center2 = self.frame.origin - plane_intersections[0].unit_direction_vector() * self.major_radius circle1 = curves.Circle3D( design3d.Frame3D(center1, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), self.minor_radius) circle2 = curves.Circle3D( design3d.Frame3D(center2, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), self.minor_radius) return [circle1, circle2]
[docs] def parallel_plane_intersection(self, plane3d: Plane3D): """ Toroidal plane intersections when plane's normal is perpendicular with the cylinder axis. :param plane3d: intersecting plane. :return: list of intersecting curves. """ distance_plane_cylinder_axis = plane3d.point_distance(self.frame.origin) if distance_plane_cylinder_axis >= self.outer_radius: return [] if plane3d.point_belongs(self.frame.origin): return self._helper_parallel_plane_intersections_through_origin(plane3d) if distance_plane_cylinder_axis > self.inner_radius: return self.concurrent_plane_intersection(plane3d, 1) point_projection = plane3d.point_projection(self.frame.origin) points = self._plane_intersection_points(plane3d) vector = (point_projection - self.frame.origin).unit_vector() frame = design3d.Frame3D(point_projection, vector, self.frame.w, vector.cross(self.frame.w)) local_points = [frame.global_to_local_coordinates(point) for point in points] lists_points = [[], []] for i, local_point in enumerate(local_points): if local_point.z > 0: lists_points[0].append(points[i]) elif local_point.z < 0: lists_points[1].append(points[i]) if math.isclose(distance_plane_cylinder_axis, self.inner_radius, abs_tol=1e-6): curves_ = [] for points in lists_points: points_ = d3d_common_operations.order_points_list_for_nearest_neighbor(points + [point_projection]) points_ = points_[points_.index(point_projection):] + points_[:points_.index(point_projection)] edge = edges.BSplineCurve3D.from_points_interpolation(points_ + [points_[0]], 6) curves_.append(edge) return curves_ curves_ = [] for points in lists_points: points_ = d3d_common_operations.order_points_list_for_nearest_neighbor(points) edge = edges.BSplineCurve3D.from_points_interpolation(points_ + [points_[0]], 6) curves_.append(edge) return curves_
[docs] def perpendicular_plane_intersection(self, plane3d): """ Toroidal plane intersections when plane's normal is parallel with the cylinder axis. :param plane3d: intersecting plane. :return: list of intersecting curves. """ distance_plane_cylinder_axis = plane3d.point_distance(self.frame.origin) if distance_plane_cylinder_axis > self.minor_radius: return [] if plane3d.point_belongs(self.frame.origin): circle1 = curves.Circle3D(self.frame, self.outer_radius) circle2 = curves.Circle3D(self.frame, self.inner_radius) return [circle1, circle2] plane1 = plane3d.rotation(plane3d.frame.origin, plane3d.frame.u, math.pi / 4) plane_intersections = plane3d.plane_intersections(plane1) torus_line_intersections = self.line_intersections(plane_intersections[0]) torus_line_intersections = plane_intersections[0].sort_points_along_curve(torus_line_intersections) center = plane3d.point_projection(self.frame.origin) if not torus_line_intersections and math.isclose(distance_plane_cylinder_axis, self.minor_radius, abs_tol=1e-6): circle = curves.Circle3D( design3d.Frame3D(center, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), self.major_radius) return [circle] radius1 = center.point_distance(torus_line_intersections[0]) circle1 = curves.Circle3D( design3d.Frame3D(center, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), radius1) if len(torus_line_intersections) == 4: radius2 = center.point_distance(torus_line_intersections[1]) circle2 = curves.Circle3D( design3d.Frame3D(center, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), radius2) return [circle1, circle2] return [circle1]
def _plane_intersection_points(self, plane3d): """ Gets the points of intersections between the plane and the toroidal surface. :param plane3d: other plane 3d. :return: points of intersections. """ axis_angle = math.degrees(design3d.geometry.vectors3d_angle(self.frame.w, plane3d.frame.w)) if 0 < axis_angle <= math.degrees(math.atan(self.minor_radius / self.major_radius)): torus_circles = self.torus_arcs(80) elif axis_angle < 45: torus_circles = self.torus_arcs(80) + self._torus_circle_generatrices_xy(80) else: torus_circles = self._torus_circle_generatrices_xy(80) points_intersections = [] for arc in torus_circles: inters = plane3d.curve_intersections(arc) for i in inters: if not i.in_list(points_intersections): points_intersections.append(i) return points_intersections
[docs] def get_villarceau_circles(self, plane3d): """ The concurrent intersecting plane touches the torus in two isolated points. :param plane3d: concurrent plane. :return: two circles. """ plane1 = Plane3D(self.frame) plane_intersections1 = plane1.plane_intersections(plane3d) torus_line_interections1 = self.line_intersections(plane_intersections1[0]) points = torus_line_interections1 radius1 = points[0].point_distance(points[2]) / 2 circle1 = curves.Circle3D(design3d.Frame3D((points[0] + points[2]) / 2, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), radius1) radius2 = points[1].point_distance(points[3]) / 2 circle2 = curves.Circle3D(design3d.Frame3D((points[1] + points[3]) / 2, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), radius2) return [circle1, circle2]
[docs] def concurrent_plane_intersection(self, plane3d, number_curves: int = None): """ Toroidal plane intersections when plane's normal is concurrent with the cone's axis, but not orthogonal. :param plane3d: intersecting plane. :param number_curves: the number of resulting curves, if known. :return: list of intersecting curves. """ if plane3d.point_distance(self.frame.origin) > self.inner_radius: torus_origin_plane = Plane3D(self.frame) projected_point_plane3d = plane3d.point_projection(self.frame.origin) torus_plane_projection = torus_origin_plane.point_projection(projected_point_plane3d) point = self.frame.origin + (torus_plane_projection - self.frame.origin).unit_vector() * self.major_radius if plane3d.point_distance(point) > self.minor_radius: return [] points_intersections = self._plane_intersection_points(plane3d) if not plane3d.point_belongs(self.frame.origin, 1e-6): point_projection = plane3d.point_projection(self.frame.origin) vector = (point_projection - self.frame.origin).unit_vector() frame = design3d.Frame3D(point_projection, vector, self.frame.w, vector.cross(self.frame.w)) plane_intersections = d3d_utils_intersections.get_two_planes_intersections(plane3d.frame, frame) line = curves.Line3D(plane_intersections[0], plane_intersections[1]) line_intersections = self.line_intersections(line) for inter in self.line_intersections(line): if not inter.in_list(points_intersections): points_intersections.append(inter) if line_intersections: number_curves = 1 if number_curves == 1: ordered_points = d3d_common_operations.order_points_list_for_nearest_neighbor(points_intersections) inters_points = [ordered_points + [ordered_points[0]]] else: inters_points = d3d_common_operations.separate_points_by_closeness(points_intersections) if len(inters_points) == 1 and plane3d.point_belongs(self.frame.origin): return self.get_villarceau_circles(plane3d) return [edges.BSplineCurve3D.from_points_interpolation(list_points, 8, centripetal=False) for list_points in inters_points]
[docs] def plane_intersections(self, plane3d): """ Toroidal intersections with a plane. :param plane3d: intersecting plane. :return: list of intersecting curves. """ projected_origin = plane3d.point_projection(self.frame.origin) translated_to_local_plane3d = plane3d.translation((projected_origin - plane3d.frame.origin).to_vector()) if math.isclose(abs(translated_to_local_plane3d.frame.w.dot(self.frame.w)), 0, abs_tol=1e-6): return self.parallel_plane_intersection(translated_to_local_plane3d) if math.isclose(abs(translated_to_local_plane3d.frame.w.dot(self.frame.w)), 1, abs_tol=1e-6): return self.perpendicular_plane_intersection(translated_to_local_plane3d) return self.concurrent_plane_intersection(translated_to_local_plane3d)
def _cylinder_intersection_points(self, cylindrical_surface: CylindricalSurface3D): """ Gets the points of intersections between the cylindrical surface and the toroidal surface. :param cylindrical_surface: other Cylindrical 3d. :return: points of intersections. """ arcs = self.torus_arcs(200) + self._torus_circle_generatrices_xy(200) points_intersections = [] for arc in arcs: intersections = cylindrical_surface.circle_intersections(arc) for intersection in intersections: if not intersection.in_list(points_intersections): points_intersections.append(intersection) for edge in cylindrical_surface.get_generatrices(300, self.outer_radius * 3): intersections = self.line_intersections(edge.line) for point in intersections: if not point.in_list(points_intersections): points_intersections.append(point) return points_intersections
[docs] def cylindricalsurface_intersections(self, cylindrical_surface: CylindricalSurface3D): """ Gets the intersections between a toroidal surface and cylindrical surface. :param cylindrical_surface: other cylindrical surface. :return: List os curves intersecting Torus. """ line = curves.Line3D.from_point_and_vector(cylindrical_surface.frame.origin, cylindrical_surface.frame.w) distance_to_self_origin = line.point_distance(self.frame.origin) if math.isclose(abs(self.frame.w.dot(cylindrical_surface.frame.w)), 1.0, abs_tol=1e-6) and \ math.isclose(distance_to_self_origin, 0.0, abs_tol=1e-6): if cylindrical_surface.radius < self.minor_radius: return [] if math.isclose(cylindrical_surface.radius, self.minor_radius, abs_tol=1e-6): return [curves.Circle3D(self.frame, self.minor_radius)] intersection_points = self._cylinder_intersection_points(cylindrical_surface) inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) curves_ = [] for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 7, centripetal=False) if isinstance(bspline.simplify, edges.FullArc3D): curves_.append(bspline.simplify) continue curves_.append(bspline) return curves_
[docs] def is_coincident(self, surface3d, abs_tol: float = 1e-6): """ Verifies if two ToroidalSurfaces are coincident. :param surface3d: surface to verify. :param abs_tol: tolerance. :return: True if they are coincident, False otherwise. """ if not isinstance(self, surface3d.__class__): return False if math.isclose(abs(self.frame.w.dot(surface3d.frame.w)), 1.0, abs_tol=abs_tol) and \ math.isclose(self.major_radius, surface3d.major_radius, abs_tol=abs_tol) and \ math.isclose(self.minor_radius, surface3d.minor_radius, abs_tol=abs_tol): return True return False
def _conical_intersection_points(self, conical_surface: 'ConicalSurface3D'): """ Gets the points of intersections between the cylindrical surface and the toroidal surface. :param conical_surface: other Conical Surface 3d. :return: points of intersections. """ arcs = self.torus_arcs(200) points_intersections = [] for arc in arcs: intersections = conical_surface.circle_intersections(arc) points_intersections.extend(intersections) point1 = conical_surface.frame.global_to_local_coordinates(design3d.Point3D(0, 0, self.bounding_box.zmin)) point2 = conical_surface.frame.global_to_local_coordinates(design3d.Point3D(0, 0, self.bounding_box.zmax)) for edge in conical_surface.get_generatrices(300, self.outer_radius * 3) + \ conical_surface.get_circle_generatrices(100, max(point1.z, 0), max(point2.z, 0)): intersections = self.edge_intersections(edge) for point in intersections: if not point.in_list(points_intersections): points_intersections.append(point) return points_intersections
[docs] def conicalsurface_intersections(self, conical_surface: 'ConicalSurface3D'): """ Gets the intersections between a toroidal surface and cylindrical surface. :param conical_surface: other Conical Surface 3d. :return: List os curves intersecting Torus. """ intersection_points = self._conical_intersection_points(conical_surface) if not intersection_points: return [] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) curves_ = [] for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 4, centripetal=False) if isinstance(bspline.simplify, edges.FullArc3D): curves_.append(bspline.simplify) continue curves_.append(bspline) return curves_
def _spherical_intersection_points(self, spherical_surface: 'SphericalSurface3D'): """ Gets the points of intersections between the spherical surface and the toroidal surface. :param spherical_surface: other Spherical Surface 3d. :return: points of intersections. """ arcs = self.torus_arcs(300) + self._torus_circle_generatrices_xy(100) intersection_points = [] for arc in arcs: intersections = spherical_surface.circle_intersections(arc) intersection_points.extend(intersections) return intersection_points
[docs] def sphericalsurface_intersections(self, spherical_surface: 'SphericalSurface3D'): """ Gets the intersections between a toroidal surface and spherical surface. :param spherical_surface: other spherical Surface 3d. :return: List os curves intersecting Torus. """ intersection_points = self._spherical_intersection_points(spherical_surface) if not intersection_points: return [] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) curves_ = [] for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 4, centripetal=False) if isinstance(bspline.simplify, edges.FullArc3D): curves_.append(bspline.simplify) continue curves_.append(bspline) return curves_
def _toroidal_intersection_points(self, toroidal_surface): """ Gets the points of intersections between the spherical surface and the toroidal surface. :param toroidal_surface: other Toroidal Surface 3d. :return: points of intersections. """ arcs = self.torus_arcs(300) + self._torus_circle_generatrices_xy(200) intersection_points = [] for arc in arcs: intersections = toroidal_surface.circle_intersections(arc) for intersection in intersections: if not intersection.in_list(intersection_points): intersection_points.append(intersection) return intersection_points
[docs] def toroidalsurface_intersections_profile_profile(self, toroidal_surface): """ Get intersections between two parallel toroidal surfaces, if there are any. :param toroidal_surface: other toroidal surface. :return: """ local_self = self.frame_mapping(self.frame, 'new') local_other_toroidal_surface = toroidal_surface.frame_mapping(self.frame, 'new') circle = local_self.torus_arcs(1)[0] circle_intersections = local_other_toroidal_surface.circle_intersections(circle) circles = [] for intersection in circle_intersections: center = design3d.Point3D(0, 0, intersection.z) circles_frame = design3d.Frame3D(center, local_self.frame.u, local_self.frame.v, local_self.frame.w) circles.append(curves.Circle3D(circles_frame, intersection.point_distance(center))) return circles
def _yvone_villarceau_circles(self, toroidal_surface): """ Gets the Yvone-Villarceau circles from two toroidal surfaces intersections. """ circle_r1 = curves.Circle3D(self.frame, self.minor_radius) circle_r2 = curves.Circle3D(toroidal_surface.frame, toroidal_surface.minor_radius) circle_intersections = circle_r1.circle_intersections(circle_r2) intersections = [] for intersection in circle_intersections: x_comp, y_comp, _ = intersection cos_s = x_comp / self.minor_radius sin_s = y_comp / self.minor_radius if toroidal_surface.frame.u.z != 0.0 and toroidal_surface.frame.v.z != 0.0: sin_t = (y_comp - toroidal_surface.frame.origin.y + (toroidal_surface.frame.origin.z * toroidal_surface.frame.u.y / toroidal_surface.frame.u.z)) * (1 / ( (toroidal_surface.frame.v.y - ( toroidal_surface.frame.v.z / toroidal_surface.frame.u.z) ) * toroidal_surface.minor_radius)) cos_t = -toroidal_surface.frame.origin.z / ( toroidal_surface.minor_radius * toroidal_surface.frame.u.z ) - sin_t * ( toroidal_surface.frame.v.z / toroidal_surface.frame.u.z) elif toroidal_surface.frame.origin.z == 0: sin_t = (y_comp - toroidal_surface.frame.origin.y ) * (1 / (toroidal_surface.frame.v.y * toroidal_surface.minor_radius)) cos_t = math.cos(math.asin(sin_t)) else: raise NotImplementedError for sign in [1, -1]: normal1 = design3d.Vector3D(-(self.minor_radius / self.major_radius) * sin_s, (self.minor_radius / self.major_radius) * cos_s, sign * math.sqrt( 1 - (self.minor_radius / self.major_radius) ** 2) ).unit_vector() normal2 = -(toroidal_surface.minor_radius / toroidal_surface.major_radius ) * sin_t * toroidal_surface.frame.u + ( toroidal_surface.minor_radius / toroidal_surface.major_radius ) * cos_t * toroidal_surface.frame.v + sign * math.sqrt( 1 - (toroidal_surface.minor_radius / toroidal_surface.major_radius) ** 2 ) * toroidal_surface.frame.w if abs(abs(normal1.dot(normal2.unit_vector())) - 1.0) < 1e-6: intersections.append(curves.Circle3D.from_center_normal( intersection, normal1, self.major_radius)) vector = (intersection - self.frame.origin).unit_vector() plane = Plane3D(design3d.Frame3D(intersection, self.frame.w, vector.cross(self.frame.w), vector)) intersections.extend(self.plane_intersections(plane)) return intersections
[docs] def outer_radius_tangent_inner_radius_toroidalsurface_intersections(self, toroidal_surface): """ Calculates the intersections between two toroidal surfaces. Case where the outer radius of one toroidal surface is touching inner radius of the other toroidal surface. :param toroidal_surface: other toroidal surface. :return: """ intersections = [] distance_origin_to_other_axis = self.frame.origin.point_distance(toroidal_surface.frame.origin) intersection_points = self._toroidal_intersection_points(toroidal_surface) vector = (toroidal_surface.frame.origin - self.frame.origin).unit_vector() point1 = self.frame.origin - vector * self.inner_radius if not point1.in_list(intersection_points): intersection_points.append(point1) point2 = self.frame.origin + vector * (distance_origin_to_other_axis + toroidal_surface.inner_radius) if not point2.in_list(intersection_points): intersection_points.append(point2) if not intersection_points: return intersections inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) frame = design3d.Frame3D(self.frame.origin, vector, self.frame.w, vector.cross(self.frame.w)) curves_ = [] for points in inters_points: local_points = [frame.global_to_local_coordinates(point) for point in points] lists_points = [[], []] first_point = None for i, local_point in enumerate(local_points): if local_point.z > 0: lists_points[0].append(points[i]) elif local_point.z < 0: lists_points[1].append(points[i]) else: first_point = points[i] if not first_point: raise NotImplementedError for list_points in lists_points: points_ = d3d_common_operations.order_points_list_for_nearest_neighbor( [first_point] + list(set(list_points))) points_ = points_[points_.index(first_point):] + points_[:points_.index(first_point)] edge = edges.BSplineCurve3D.from_points_interpolation(points_ + [points_[0]], 8) curves_.append(edge) return curves_
[docs] def toroidalsurface_intersections(self, toroidal_surface): """ Gets the intersections between two toroidal surface. :param toroidal_surface: other toroidal Surface 3d. :return: List os curves intersecting Torus. """ intersections = [] axis_line = curves.Line3D.from_point_and_vector(self.frame.origin, self.frame.w) distance_origin_to_other_axis = self.frame.origin.point_distance(toroidal_surface.frame.origin) is_minor_same = abs(self.minor_radius - toroidal_surface.minor_radius) < 1e-6 is_major_same = abs(self.major_radius - toroidal_surface.major_radius) < 1e-6 if math.isclose(abs(self.frame.w.dot(toroidal_surface.frame.w)), 1.0, abs_tol=1e-6): if d3d_common_operations.get_plane_point_distance(self.frame, toroidal_surface.frame.origin) > \ self.minor_radius + toroidal_surface.minor_radius: return [] if axis_line.point_distance(toroidal_surface.frame.origin) < 1e-6: return self.toroidalsurface_intersections_profile_profile(toroidal_surface) if is_minor_same and \ abs(distance_origin_to_other_axis - self.major_radius - toroidal_surface.major_radius) < 1e-6: vector = (toroidal_surface.frame.origin - self.frame.origin).unit_vector() center = self.frame.origin + vector * self.major_radius circle = curves.Circle3D(design3d.Frame3D(center, vector, self.frame.w, vector.cross(self.frame.w)), self.minor_radius) if is_major_same: plane = Plane3D(design3d.Frame3D(center, self.frame.w, vector.cross(self.frame.w), vector)) intersections.extend(self.plane_intersections(plane)) intersections.append(circle) elif is_major_same and \ abs(distance_origin_to_other_axis - self.minor_radius - toroidal_surface.minor_radius) < 1e-6: if is_minor_same: intersections = self._yvone_villarceau_circles(toroidal_surface) if intersections: return intersections return self.outer_radius_tangent_inner_radius_toroidalsurface_intersections(toroidal_surface) elif (is_minor_same and abs(self.frame.w.dot((toroidal_surface.frame.origin - self.frame.origin).unit_vector())) < 1e-6 and distance_origin_to_other_axis - self.outer_radius < toroidal_surface.inner_radius): circle_bigr1 = curves.Circle3D(self.frame, self.major_radius + self.minor_radius) circle_bigr2 = curves.Circle3D(toroidal_surface.frame, toroidal_surface.major_radius + toroidal_surface.minor_radius) circle_intersections = circle_bigr1.circle_intersections(circle_bigr2) if circle_intersections: center = (circle_intersections[0] + circle_intersections[1]) / 2 vector = (center - self.frame.origin).unit_vector() plane = Plane3D(design3d.Frame3D(center, self.frame.w, vector.cross(self.frame.w), vector)) intersections = self.plane_intersections(plane) intersection_points = self._toroidal_intersection_points(toroidal_surface) if not intersection_points: return intersections if intersections: intersection_points = [point for point in intersection_points if not any( intersection.point_belongs(point, 1e-4) for intersection in intersections)] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 8, centripetal=False) intersections.append(bspline) return intersections
[docs] def u_iso(self, u: float) -> curves.Circle3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A circle 3D :rtype: :class:`curves.Circle3D` """ center_u0 = self.frame.origin + self.frame.u * self.major_radius center = center_u0.rotation(self.frame.origin, self.frame.w, u) u_vector = (center - self.frame.origin).unit_vector() frame = design3d.Frame3D(center, u_vector, self.frame.w, u_vector.cross(self.frame.w)) return curves.Circle3D(frame, self.minor_radius)
[docs] def v_iso(self, v: float) -> curves.Circle3D: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A Circle 3D :rtype: :class:`curves.Circle3D` """ z = self.minor_radius * math.sin(v) frame = self.frame.translation(self.frame.w * z) radius = abs(self.major_radius + self.minor_radius * math.cos(v)) return curves.Circle3D(frame, radius)
[docs] def normal_at_point(self, point: design3d.Point3D): """ Gets normal vector at given point on the surface. :param point: point to be verified. :return: normal """ if not self.point_belongs(point): raise ValueError('Point given not on surface.') theta, phi = self.point3d_to_2d(point) normal = math.cos(phi) * (math.cos(theta) * self.frame.u + math.sin(theta) * self.frame.v) + math.sin(phi) * self.frame.w return normal
[docs] class ConicalSurface3D(UPeriodicalSurface): """ Describes a cone. A cone is defined by the half-angle, and is positioned in space by a frame and a reference radius. The main axis of the frame is the axis of revolution of the cone. The plane defined by the origin, the x direction and the y direction of the frame is the plane of the cone. The intersection of the cone with this reference plane is a circle of radius equal to the reference radius. The apex of the cone is on the negative side of the main axis of the frame if the half-angle is positive, and on the positive side if the half-angle is negative. This frame is the "local coordinate system" of the cone. The following apply: Rotation around its main axis, in the trigonometric sense given by the x direction and the y direction, defines the u parametric direction and the x-axis gives the origin for the u parameter. The z axis defines the v parametric direction of the cone and the origin of the frame is the origin of the v parameter. The parametric range of the two parameters is: - [ 0, 2.*Pi ] for u, and - ] -infinity, +infinity [ for v The parametric equation of the cone is: P(u, v) = O + (R + v*tan(ang)) * (cos(u)*x + sin(u)*y) + v*z where: - O, x, y and z are respectively the origin, the x, y and z direction of the cone's local coordinate system - ang is the half-angle at the apex of the cone - R is the reference radius. :param frame: Cone's local coordinate system. :param semi_angle: half-angle at the apex of the cone. :param ref_radius: radius of the circle formed by the intersection of the cone with the reference plane. """ face_class = 'ConicalFace3D' x_periodicity = design3d.TWO_PI y_periodicity = None def __init__(self, frame: design3d.Frame3D, semi_angle: float, ref_radius: float = 0.0, name: str = ''): self.semi_angle = semi_angle self.ref_radius = ref_radius UPeriodicalSurface.__init__(self, frame=frame, name=name) def __hash__(self): return hash((self.__class__.__name__, self.frame, self.semi_angle, self.ref_radius)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.frame == other.frame and self.semi_angle == other.semi_angle and self.ref_radius == self.ref_radius: return True return False @property def u_domain(self): """The parametric domain of the surface in the U direction.""" return -math.pi, math.pi @property def v_domain(self): """The parametric domain of the surface in the V direction.""" return -math.inf, math.inf @property def domain(self): """Returns u and v bounds.""" return -math.pi, math.pi, -math.inf, math.inf @property def apex(self): """ Computes the apex of the cone. It is on the negative side of the axis of revolution of this cone if the half-angle at the apex is positive, and on the positive side of the "main axis" if the half-angle is negative. """ origin = self.frame.origin return origin + (-self.ref_radius / math.tan(self.semi_angle)) * self.frame.w
[docs] def get_generatrices(self, number_lines: int = 36, z: float = 1): """ Gets Conical Surface 3D generatrix lines. :param z: cone's z height. :param number_lines: number of generatrix lines. :return: """ v = z - self.ref_radius / math.tan(self.semi_angle) point1 = self.apex point2 = self.point2d_to_3d(design3d.Point2D(0.0, v)) generatrix = edges.LineSegment3D(point1, point2) list_generatrices = [generatrix] for i in range(1, number_lines): theta = i / number_lines * design3d.TWO_PI wire = generatrix.rotation(self.frame.origin, self.frame.w, theta) list_generatrices.append(wire) return list_generatrices
[docs] def get_circle_generatrices(self, number_circles: int, z1, z2): """ Get circles generatrix of the cone. :param z1: Initial height of cone. :param z2: Final height of cone. :param number_circles: number of expected circles. """ circles = [] for i_z in np.linspace(z1, z2, number_circles): circle = self.v_iso(i_z) if circle is None: continue circles.append(circle) return circles
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), **kwargs): """ Plots the ConicalSurface3D. """ z = kwargs.get("z", 1) if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') self.frame.plot(ax) line_generatrices = self.get_generatrices(36, z) _, z_apex = self.point3d_to_2d(self.apex) circle_generatrices = self.get_circle_generatrices(50, z_apex, z_apex + z) for edge in line_generatrices + circle_generatrices: edge.plot(ax, edge_style) return ax
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a ConicalSurface3D. :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 ConicalSurface3D object. :rtype: :class:`design3d.faces.ConicalSurface3D` """ length_conversion_factor = kwargs.get("length_conversion_factor", 1) angle_conversion_factor = kwargs.get("angle_conversion_factor", 1) frame = object_dict[arguments[1]] radius = float(arguments[2]) * length_conversion_factor semi_angle = float(arguments[3]) * angle_conversion_factor return cls(frame, semi_angle, radius, name=arguments[0][1:-1])
[docs] def is_coincident(self, surface3d, abs_tol: float = 1e-6): """ Verifies if two conical surfaces are coincident. :param surface3d: other surface 3d. :param abs_tol: tolerance. :return: True if they are coincident, False otherwise. """ if not isinstance(surface3d, ConicalSurface3D): return False if math.isclose(self.frame.w.dot(surface3d.frame.w), 1.0, abs_tol=abs_tol) and \ self.frame.origin.is_close(surface3d.frame.origin) and \ math.isclose(self.semi_angle, surface3d.semi_angle, abs_tol=abs_tol) and \ math.isclose(self.ref_radius, surface3d.ref_radius, abs_tol=abs_tol): return True return False
[docs] def to_step(self, current_id): """ Converts the object to a STEP representation. :param current_id: The ID of the last written primitive. :type current_id: int :return: The STEP representation of the object and the last ID. :rtype: tuple[str, list[int]] """ content, frame_id = self.frame.to_step(current_id) current_id = frame_id + 1 content += f"#{current_id} = CONICAL_SURFACE('{self.name}',#{frame_id},{self.ref_radius},{self.semi_angle});\n" return content, [current_id]
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new ConicalSurface3D. :param side: 'old' or 'new' """ new_frame = self.frame.frame_mapping(frame, side) return ConicalSurface3D(new_frame, self.semi_angle, self.ref_radius, name=self.name)
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Coverts a parametric coordinate on the surface into a 3D spatial point (x, y, z). :param point2d: Point at the ConicalSuface3D :type point2d: `design3d.`Point2D` """ theta, z = point2d radius = math.tan(self.semi_angle) * z + self.ref_radius new_point = design3d.Point3D(radius * math.cos(theta), radius * math.sin(theta), z) return self.frame.local_to_global_coordinates(new_point)
[docs] def point3d_to_2d(self, point3d: design3d.Point3D): """ Returns the cylindrical coordinates design3d.Point2D(theta, z) of a Cartesian coordinates point (x, y, z). :param point3d: Point at the CylindricalSuface3D. :type point3d: :class:`design3d.`Point3D` """ x, y, z = self.frame.global_to_local_coordinates(point3d) # Do not delete this, mathematical problem when x and y close to zero (should be zero) but not 0 # Generally this is related to uncertainty of step files. if x != 0.0 and abs(x) < 1e-12: x = 0.0 if y != 0.0 and abs(y) < 1e-12: y = 0.0 if x == 0.0 and y == 0.0: theta = 0.0 else: theta = math.atan2(y, x) if abs(theta) < 1e-16: theta = 0.0 if abs(z) < 1e-16: z = 0.0 return design3d.Point2D(theta, z)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the conical surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the conical surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the conical surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ center = np.asarray(self.frame.origin) x = np.asarray([self.frame.u[0], self.frame.u[1], self.frame.u[2]]) y = np.asarray([self.frame.v[0], self.frame.v[1], self.frame.v[2]]) z = np.asarray([self.frame.w[0], self.frame.w[1], self.frame.w[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] v_values = points[:, 1] x_component = np.cos(u_values) * x y_component = np.sin(u_values) * y return (center + (v_values * math.tan(self.semi_angle) + self.ref_radius) * (x_component + y_component) + v_values * z)
[docs] def rectangular_cut(self, theta1: float, theta2: float, param_z1: float, param_z2: float, name: str = ''): """Deprecated method, Use ConicalFace3D from_surface_rectangular_cut method.""" raise AttributeError("ConicalSurface3D.rectangular_cut is deprecated." "Use the class_method from_surface_rectangular_cut in ConicalFace3D instead")
[docs] def linesegment3d_to_2d(self, linesegment3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(linesegment3d.start) end = self.point3d_to_2d(linesegment3d.end) if math.isclose(start.y, end.y, rel_tol=0.005): # special case when there is a small line segment that should be a small arc of circle instead return [edges.LineSegment2D(start, end)] if start.x != end.x: end = design3d.Point2D(start.x, end.y) if start != end: return [edges.LineSegment2D(start, end)] return None
[docs] def contour3d_to_2d(self, contour3d, return_primitives_mapping: bool = False): """ Transforms a Contour3D into a Contour2D in the parametric domain of the surface. :param contour3d: The contour to be transformed. :type contour3d: :class:`wires.Contour3D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 2D contour object. :rtype: :class:`wires.Contour2D` """ contour3d = self.check_primitives_order(contour3d) primitives2d, primitives_mapping = self.primitives3d_to_2d(contour3d.primitives) wire2d = wires.Wire2D(primitives2d) delta_x = abs(wire2d.primitives[0].start.x - wire2d.primitives[-1].end.x) if math.isclose(delta_x, design3d.TWO_PI, abs_tol=1e-3) and wire2d.is_ordered(): if len(primitives2d) > 1: # very specific conical case due to the singularity in the point z = 0 on parametric domain. if self.is_singularity_point(self.point2d_to_3d(primitives2d[-2].start)): self.repair_primitives_periodicity(primitives2d, primitives_mapping) if return_primitives_mapping: return wires.Contour2D(primitives2d), primitives_mapping return wires.Contour2D(primitives2d) # Fix contour self.repair_primitives_periodicity(primitives2d, primitives_mapping) if return_primitives_mapping: return wires.Contour2D(primitives2d), primitives_mapping return wires.Contour2D(primitives2d)
[docs] def translation(self, offset: design3d.Vector3D): """ ConicalSurface3D translation. :param offset: translation vector. :return: A new translated ConicalSurface3D. """ return self.__class__(self.frame.translation(offset), self.semi_angle, self.ref_radius)
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ ConicalSurface3D rotation. :param center: rotation center. :param axis: rotation axis. :param angle: angle rotation. :return: a new rotated ConicalSurface3D. """ new_frame = self.frame.rotation(center=center, axis=axis, angle=angle) return self.__class__(new_frame, self.semi_angle, self.ref_radius)
[docs] def circle_intersections(self, circle: curves.Circle3D): """ Calculates the intersections between a conical surface and a Circle 3D. :param circle: other circle to verify intersections. :return: a list of intersection points, if there exists any. """ if not self.frame.origin.is_close(design3d.O3D) or not self.frame.w.is_close(design3d.Z3D): local_surface = self.frame_mapping(self.frame, 'new') local_curve = circle.frame_mapping(self.frame, 'new') local_intersections = local_surface.circle_intersections(local_curve) global_intersections = [] for intersection in local_intersections: global_intersections.append(self.frame.local_to_global_coordinates(intersection)) return global_intersections if circle.bounding_box.zmax < self.frame.origin.z: return [] z_max = circle.bounding_box.zmax radius = z_max * math.tan(self.semi_angle) + self.ref_radius line = curves.Line3D.from_point_and_vector(self.frame.origin, self.frame.w) if line.point_distance(circle.center) > radius + circle.radius: return [] intersections = [point for point in self.curve_intersections(circle) if point.z >= 0] return intersections
def _full_line_intersections(self, line: curves.Line3D): """ Calculates the intersections between a conical surface and a Line 3D, for the two lobes of the cone. :param line: other line to verify intersections. :return: a list of intersection points, if there exists any. """ apex = self.apex if line.point_belongs(apex): return [apex] line_direction_vector = line.unit_direction_vector() plane_normal = line_direction_vector.cross((apex - line.point1).to_vector()).unit_vector() if self.frame.w.dot(plane_normal) > 0: plane_normal = - plane_normal plane = Plane3D.from_normal(apex, plane_normal) cos_theta = math.sqrt(1 - (plane_normal.dot(self.frame.w) ** 2)) if cos_theta >= math.cos(self.semi_angle): plane_h = Plane3D.from_normal(apex + self.frame.w, self.frame.w) circle = self.perpendicular_plane_intersection(plane_h)[0] line_p = plane_h.plane_intersections(plane)[0] circle_line_p_intersections = circle.line_intersections(line_p) intersections = [] for intersection in circle_line_p_intersections: line_v_x = curves.Line3D(apex, intersection) line_inter = line_v_x.intersection(line) if not line_inter: continue intersections.append(line_inter) return line.sort_points_along_curve(intersections) return []
[docs] def line_intersections(self, line: curves.Line3D): """ Calculates the intersections between a conical surface and a Line 3D. :param line: other line to verify intersections. :return: a list of intersection points, if there exists any. """ line_intersections = self._full_line_intersections(line) positive_lobe_intersections = [] for point in line_intersections: local_point = self.frame.global_to_local_coordinates(point) zmin = - self.ref_radius / math.tan(self.semi_angle) if local_point.z < zmin: continue positive_lobe_intersections.append(point) return positive_lobe_intersections
def _helper_parallel_plane_intersection_through_origin(self, plane): """ Conical plane intersections when plane's normal is perpendicular with the Cone's axis passing through origin. :param plane: intersecting plane. :return: list of intersecting curves """ direction = self.frame.w.cross(plane.normal) point1 = self.frame.origin + direction point2 = self.frame.origin - direction theta1 = math.atan2(point1.y, point1.x) theta2 = math.atan2(point2.y, point2.x) point1_line1 = self.point2d_to_3d(design3d.Point2D(theta1, -0.1)) point2_line1 = self.point2d_to_3d(design3d.Point2D(theta1, 0.1)) point1_line2 = self.point2d_to_3d(design3d.Point2D(theta2, -0.1)) point2_line2 = self.point2d_to_3d(design3d.Point2D(theta2, 0.1)) return [curves.Line3D(point1_line1, point2_line1), curves.Line3D(point1_line2, point2_line2)] def _hyperbola_helper(self, plane3d, hyperbola_center, hyperbola_positive_vertex): semi_major_axis = hyperbola_center.point_distance(hyperbola_positive_vertex) circle = self.v_iso(2 * semi_major_axis) hyperbola_points = plane3d.circle_intersections(circle) if not hyperbola_points: return [] semi_major_dir = (hyperbola_positive_vertex - hyperbola_center).unit_vector() frame = design3d.Frame3D(hyperbola_center, semi_major_dir, plane3d.frame.w.cross(semi_major_dir), plane3d.frame.w) local_point = frame.global_to_local_coordinates(hyperbola_points[0]) return [curves.Hyperbola3D(frame, semi_major_axis, math.sqrt((local_point.y ** 2) / (local_point.x ** 2 / semi_major_axis ** 2 - 1)))] def _parallel_plane_intersections_hyperbola_helper(self, plane): """ Conical plane intersections when plane's normal is perpendicular with the Cone's axis. :param plane: intersecting plane. :return: list containing the resulting intersection hyperbola curve. """ hyperbola_center = plane.point_projection(self.apex) z = ((math.sqrt(hyperbola_center.x ** 2 + hyperbola_center.y ** 2) - self.ref_radius) / math.tan(self.semi_angle)) hyperbola_positive_vertex = self.frame.local_to_global_coordinates( design3d.Point3D(hyperbola_center.x, hyperbola_center.y, z)) return self._hyperbola_helper(plane, hyperbola_center, hyperbola_positive_vertex)
[docs] def parallel_plane_intersection(self, plane3d: Plane3D): """ Conical plane intersections when plane's normal is perpendicular with the Cone's axis. :param plane3d: intersecting plane :return: list of intersecting curves """ if plane3d.point_belongs(self.frame.origin): return self._helper_parallel_plane_intersection_through_origin(plane3d) if not self.frame.w.is_close(design3d.Z3D): local_surface = self.frame_mapping(self.frame, 'new') local_plane = plane3d.frame_mapping(self.frame, 'new') local_intersections = local_surface.parallel_plane_intersection(local_plane) return [intersection.frame_mapping(self.frame, 'old') for intersection in local_intersections] return self._parallel_plane_intersections_hyperbola_helper(plane3d)
[docs] def perpendicular_plane_intersection(self, plane3d): """ Cone plane intersections when plane's normal is parallel with the cone axis. :param plane3d: Intersecting plane. :return: List of intersecting curves. """ center3d_plane = plane3d.point_projection(self.frame.origin) radius = self.frame.origin.point_distance(center3d_plane) * math.tan(self.semi_angle) + self.ref_radius circle3d = curves.Circle3D(design3d.Frame3D(center3d_plane, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), radius) return [circle3d]
def _concurrent_plane_intersection_parabola(self, plane3d, parabola_vertex): """ Calculates parabola for Cone and concurrent plane intersections. :param plane3d: intersecting plane. :param parabola_vertex: parabla vertex point. :return: list of intersecting curves. """ distance_plane_vertex = parabola_vertex.point_distance(self.apex) circle = self.perpendicular_plane_intersection( Plane3D(design3d.Frame3D(self.frame.origin + distance_plane_vertex * 5 * self.frame.w, self.frame.u, self.frame.v, self.frame.w)))[0] line_circle_intersecting_plane = d3d_utils_intersections.get_two_planes_intersections( plane3d.frame, circle.frame) line_circle_intersecting_plane = curves.Line3D(line_circle_intersecting_plane[0], line_circle_intersecting_plane[1]) parabola_points = circle.line_intersections(line_circle_intersecting_plane) v_vector = ((parabola_points[0] + parabola_points[1]) / 2 - parabola_vertex).unit_vector() frame = design3d.Frame3D(parabola_vertex, v_vector.cross(plane3d.frame.w), v_vector, plane3d.frame.w) local_point = frame.global_to_local_coordinates(parabola_points[0]) vrtx_equation_a = local_point.y / local_point.x ** 2 parabola = curves.Parabola3D(frame, 1 / (4 * vrtx_equation_a)) return [parabola]
[docs] def concurrent_plane_intersection(self, plane3d: Plane3D): """ Cone plane intersections when plane's normal is concurrent with the cone's axis, but not orthogonal. :param plane3d: intersecting plane. :return: list of intersecting curves. """ plane_normal = self.frame.w.cross(plane3d.frame.w) plane2 = Plane3D.from_normal(plane3d.frame.origin, plane_normal) plane2_plane3d_intersections = plane3d.plane_intersections(plane2) line_intersections = self.line_intersections(plane2_plane3d_intersections[0]) if 1 > len(line_intersections) or len(line_intersections) > 2: return [] angle_plane_cones_direction = abs(design3d.geometry.vectors3d_angle(self.frame.w, plane3d.frame.w) - math.pi / 2) if math.isclose(angle_plane_cones_direction, self.semi_angle, abs_tol=1e-8): return self._concurrent_plane_intersection_parabola(plane3d, line_intersections[0]) if len(line_intersections) == 1: full_line_intersections = self._full_line_intersections(plane2_plane3d_intersections[0]) if len(full_line_intersections) == 1: return [] hyperbola_center = (full_line_intersections[0] + full_line_intersections[1]) / 2 return self._hyperbola_helper(plane3d, hyperbola_center, line_intersections[0]) if len(line_intersections) != 2: return [] ellipse_center = (line_intersections[0] + line_intersections[1]) / 2 line_intersections2 = self.line_intersections(curves.Line3D.from_point_and_vector( ellipse_center, plane_normal)) major_dir = (line_intersections[0] - ellipse_center).unit_vector() major_axis = ellipse_center.point_distance(line_intersections[0]) minor_dir = (line_intersections2[0] - ellipse_center).unit_vector() minor_axis = ellipse_center.point_distance(line_intersections2[0]) if minor_axis > major_axis: major_axis, minor_axis = minor_axis, major_axis major_dir, minor_dir = minor_dir, major_dir return [curves.Ellipse3D(major_axis, minor_axis, design3d.Frame3D( ellipse_center, major_dir, minor_dir, plane3d.frame.w))]
[docs] def plane_intersections(self, plane3d): """ Gets the intersections between a plane 3d and a conical surface 3d. :param plane3d: other plane, to verify intersections. :return: """ if math.isclose(abs(plane3d.frame.w.dot(self.frame.w)), 0, abs_tol=1e-6): return self.parallel_plane_intersection(plane3d) if math.isclose(abs(plane3d.frame.w.dot(self.frame.w)), 1, abs_tol=1e-6): return self.perpendicular_plane_intersection(plane3d) return self.concurrent_plane_intersection(plane3d)
[docs] def is_singularity_point(self, point, *args, **kwargs): """Verifies if point is on the surface singularity.""" tol = kwargs.get("tol", 1e-6) return self.apex.is_close(point, tol)
[docs] def check_primitives_order(self, contour): """ If contours passes at the cone singularity this makes sure that the contour is not in an undefined order. """ pos = 0 for i, primitive in enumerate(contour.primitives): if self.is_singularity_point(primitive.start): pos = i break if pos: contour.primitives = contour.primitives[pos:] + contour.primitives[:pos] return contour
[docs] @staticmethod def get_singularity_lines(): """ Return lines that are parallel and coincident with surface singularity at parametric domain. """ return [curves.Line2D(design3d.Point2D(-math.pi, 0), design3d.Point2D(math.pi, 0))]
def _spherical_intersection_points(self, spherical_surface: 'SphericalSurface3D'): """ Gets the points of intersections between the spherical surface and the toroidal surface. :param spherical_surface: other Spherical Surface 3d. :return: points of intersections. """ point1 = self.frame.global_to_local_coordinates(design3d.Point3D(0, 0, spherical_surface.bounding_box.zmin)) point2 = self.frame.global_to_local_coordinates(design3d.Point3D(0, 0, spherical_surface.bounding_box.zmax)) cone_generatrices = self.get_generatrices(200, spherical_surface.radius * 4) + \ self.get_circle_generatrices(200, max(point1.z, 0), max(point2.z, 0)) intersection_points = [] for gene in cone_generatrices: intersections = spherical_surface.edge_intersections(gene) for intersection in intersections: if not intersection.in_list(intersection_points): intersection_points.append(intersection) return intersection_points
[docs] def sphericalsurface_intersections(self, spherical_surface: 'SphericalSurface3D'): """ Conical Surface intersections with a Spherical surface. :param spherical_surface: intersecting sphere. :return: list of intersecting curves. """ intersection_points = self._spherical_intersection_points(spherical_surface) if not intersection_points: return [] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) curves_ = [] for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 4, centripetal=False) if isinstance(bspline.simplify, edges.FullArc3D): curves_.append(bspline.simplify) continue curves_.append(bspline) return curves_
def _conical_intersection_points(self, conical_surface: 'ConicalSurface3D', length: float): """ Gets the points of intersections between the spherical surface and the toroidal surface. :param conical_surface: other Spherical Surface 3d. :return: points of intersections. """ cone_generatrices = self.get_generatrices(max(100, int((length / 2) * 10)), length) + \ self.get_circle_generatrices(max(200, int((length / 2) * 20)), 0, length) intersection_points = [] for gene in cone_generatrices: intersections = conical_surface.edge_intersections(gene) for intersection in intersections: if not intersection.in_list(intersection_points): intersection_points.append(intersection) return intersection_points
[docs] def parallel_conicalsurface_intersections(self, conical_surface): """ Get Conical Surface intersections with another conical surface, when their axis are parallel. :param conical_surface: intersecting conical surface. :return: list of intersecting curves. """ generatrix = conical_surface.get_generatrices(z=2, number_lines=1)[0] line_intersections = self.line_intersections(generatrix.line) if line_intersections: local_surface = self.frame_mapping(self.frame, 'new') local_point = self.frame.global_to_local_coordinates(line_intersections[0]) local_circle = local_surface.v_iso(local_point.z) return [local_circle.frame_mapping(self.frame, 'old')] axis_line = curves.Line3D.from_point_and_vector(self.frame.origin, self.frame.w) if axis_line.point_distance(conical_surface.frame.origin) < 1e-6: return [] intersections_points = [self.circle_intersections(circle) for circle in [conical_surface.v_iso(1), conical_surface.v_iso(2)]] plane = Plane3D.from_3_points(intersections_points[0][0], intersections_points[0][1], intersections_points[1][0]) return self.plane_intersections(plane)
[docs] def same_apex_conicalsurface_intersections(self, conical_surface): """ Gets Conical Surface intersections with another conical surface, sharing the same apex. :param conical_surface: intersecting conical surface. :return: list of intersecting curves. """ circle = self.v_iso(1) circle_intersections = conical_surface.circle_intersections(circle) if not circle_intersections: return [] apex = self.apex return [curves.Line3D(apex, circle_intersections[0]), curves.Line3D(apex, circle_intersections[1])]
[docs] def conicalsurface_intersections(self, conical_surface): """ Conical Surface intersections with another conical surface. :param conical_surface: intersecting conical surface. :return: list of intersecting curves. """ if self.frame.w.is_colinear_to(conical_surface.frame.w): return self.parallel_conicalsurface_intersections(conical_surface) if self.apex.is_close(conical_surface.apex): return self.same_apex_conicalsurface_intersections(conical_surface) if self.semi_angle + conical_surface.semi_angle > design3d.geometry.vectors3d_angle( self.frame.w, conical_surface.frame.w): intersection_points = self._conical_intersection_points(conical_surface, 5) local_intersections = [self.frame.global_to_local_coordinates(point) for point in intersection_points] max_z_point = design3d.O3D for point in local_intersections: if point.z > max_z_point.z: max_z_point = point point_index = local_intersections.index(max_z_point) removed_point = intersection_points.pop(point_index) intersection_points.insert(0, removed_point) list_points = d3d_common_operations.order_points_list_for_nearest_neighbor(intersection_points) bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 3, centripetal=True) return [bspline] intersection_points = self._conical_intersection_points(conical_surface, 5) if not intersection_points: return [] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) curves_ = [] for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 4, centripetal=False) curves_.append(bspline) return curves_
[docs] def u_iso(self, u: float) -> curves.Line3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A line 3D :rtype: :class:`curves.Line3D` """ point1_at_u = self.point2d_to_3d(design3d.Point2D(u, 0.0)) point2_at_u = self.point2d_to_3d(design3d.Point2D(u, 0.001)) return curves.Line3D(point1_at_u, point2_at_u)
[docs] def v_iso(self, v: float) -> curves.Circle3D: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A Circle 3D :rtype: :class:`curves.Circle3D` """ radius = abs(self.ref_radius + v * math.tan(self.semi_angle)) if radius < 1e-15: return None frame = self.frame.translation(self.frame.w * v) return curves.Circle3D(frame, radius)
[docs] def normal_at_point(self, point: design3d.Point3D): """ Gets normal vector at given point on the surface. :param point: point to be verified. :return: normal """ if not self.point_belongs(point): raise ValueError('Point given not on surface.') theta, z_apex = self.point3d_to_2d(point) normal = (math.cos(theta) * self.frame.u + math.sin(theta) * self.frame.v - math.tan(self.semi_angle) * self.frame.w) / (math.sqrt(1 + math.tan(self.semi_angle)**2)) if self.ref_radius + z_apex * math.tan(self.semi_angle) < 0: return - normal return normal
[docs] class SphericalSurface3D(UVPeriodicalSurface): """ Defines a spherical surface. :param frame: Sphere's frame to position it :type frame: design3d.Frame3D :param radius: Sphere's radius :type radius: float """ face_class = 'SphericalFace3D' x_periodicity = design3d.TWO_PI y_periodicity = math.pi def __init__(self, frame, radius, name=''): self.radius = radius UVPeriodicalSurface.__init__(self, frame=frame, name=name) # Hidden Attributes self._bbox = None def __hash__(self): return hash((self.__class__.__name__, self.frame, self.radius)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.frame == other.frame and self.radius == other.radius: return True return False @property def domain_u(self): """The parametric domain of the surface in the U direction.""" return -math.pi, math.pi @property def domain_v(self): """The parametric domain of the surface in the V direction.""" return -math.pi, math.pi def _circle_generatrices(self, number_circles: int): """ Gets the sphere circle generatrices. :param number_circles: number of circles to be created. :return: List of Circle 3D. """ return [self.u_iso(theta) for theta in np.linspace(0, math.pi, number_circles)] def _circle_generatrices_xy(self, number_circles: int): """ Gets the sphere circle generatrices in parallel planes. :param number_circles: number of circles to be created. :return: List of Circle 3D. """ phi_angles = np.linspace(-0.5 * math.pi, 0.5 * math.pi, number_circles + 2) return [self.v_iso(phi) for phi in phi_angles[1:-1]] @property def domain(self): """Returns u and v bounds.""" return -math.pi, math.pi, -0.5 * math.pi, 0.5 * math.pi @property def bounding_box(self): """Bounding Box for Spherical Surface 3D.""" if not self._bbox: self._bbox = self._bounding_box() return self._bbox def _bounding_box(self): points = [self.frame.origin + design3d.Point3D(-self.radius, -self.radius, -self.radius), self.frame.origin + design3d.Point3D(self.radius, self.radius, self.radius), ] return design3d.core.BoundingBox.from_points(points)
[docs] def get_circle_at_z(self, z_position: float): """ Gets a circle on the sphere at given z position < radius. :param z_position: circle's z position. :return: circle 3D at given z position. """ center1 = self.frame.origin.translation(self.frame.w * z_position) circle_radius = math.sqrt(self.radius ** 2 - center1.point_distance(self.frame.origin) ** 2) circle = curves.Circle3D(design3d.Frame3D(center1, self.frame.u, self.frame.v, self.frame.w), circle_radius) return circle
[docs] def contour2d_to_3d(self, contour2d, return_primitives_mapping: bool = False): """ Transforms a Contour2D in the parametric domain of the surface into a Contour3D in Cartesian coordinate. :param contour2d: The contour to be transformed. :type contour2d: :class:`wires.Contour2D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 3D contour object. :rtype: :class:`wires.Contour3D` """ primitives3d = [] primitives_mapping = {} for primitive2d in contour2d.primitives: if self.is_degenerated_brep(primitive2d) or primitive2d.name == "construction": continue method_name = f'{primitive2d.__class__.__name__.lower()}_to_3d' if hasattr(self, method_name): try: primitives_list = getattr(self, method_name)(primitive2d) if primitives_list: primitives3d.extend(primitives_list) else: continue primitives_mapping[primitive2d] = primitives_list[0] except AttributeError: print(f'Class {self.__class__.__name__} does not implement {method_name}' f'with {primitive2d.__class__.__name__}') else: raise AttributeError(f'Class {self.__class__.__name__} does not implement {method_name}') if return_primitives_mapping: return wires.Contour3D(primitives3d), primitives_mapping return wires.Contour3D(primitives3d)
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a SphericalSurface3D. :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 SphericalSurface3D object. :rtype: :class:`design3d.faces.SphericalSurface3D` """ length_conversion_factor = kwargs.get("length_conversion_factor", 1) frame = object_dict[arguments[1]] radius = float(arguments[2]) * length_conversion_factor return cls(frame, radius, arguments[0][1:-1])
[docs] def to_step(self, current_id): """ Converts the object to a STEP representation. :param current_id: The ID of the last written primitive. :type current_id: int :return: The STEP representation of the object and the last ID. :rtype: tuple[str, list[int]] """ content, frame_id = self.frame.to_step(current_id) current_id = frame_id + 1 content += f"#{current_id} = SPHERICAL_SURFACE('{self.name}',#{frame_id},{round(1000 * self.radius, 4)});\n" return content, [current_id]
[docs] def point2d_to_3d(self, point2d): """ Coverts a parametric coordinate on the surface into a 3D spatial point (x, y, z). source: https://mathcurve.com/surfaces/sphere # -pi<theta<pi, -pi/2<phi<pi/2 :param point2d: Point at the CylindricalSuface3D. :type point2d: `design3d.`Point2D` """ theta, phi = point2d x = self.radius * math.cos(phi) * math.cos(theta) y = self.radius * math.cos(phi) * math.sin(theta) z = self.radius * math.sin(phi) return self.frame.local_to_global_coordinates(design3d.Point3D(x, y, z))
[docs] def point3d_to_2d(self, point3d): """ Transform a 3D spatial point (x, y, z) into a 2D spherical parametric point (theta, phi). """ x, y, z = self.frame.global_to_local_coordinates(point3d) z = min(self.radius, max(-self.radius, z)) if z == -0.0: z = 0.0 # Do not delete this, mathematical problem when x and y close to zero (should be zero) but not 0 # Generally this is related to uncertainty of step files. if abs(x) < 1e-7: x = 0 if abs(y) < 1e-7: y = 0 theta = math.atan2(y, x) if abs(theta) < 1e-10: theta = 0 z_over_r = z / self.radius phi = math.asin(z_over_r) if abs(phi) < 1e-10: phi = 0 return design3d.Point2D(theta, phi)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the spherical surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the spherical surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the spherical surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ center = np.asarray(self.frame.origin) x = np.asarray([self.frame.u[0], self.frame.u[1], self.frame.u[2]]) y = np.asarray([self.frame.v[0], self.frame.v[1], self.frame.v[2]]) z = np.asarray([self.frame.w[0], self.frame.w[1], self.frame.w[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] v_values = points[:, 1] common_term = self.radius * np.cos(v_values) x_component = np.cos(u_values) * x y_component = np.sin(u_values) * y z_component = self.radius * np.sin(v_values) * z return center + common_term * (x_component + y_component) + z_component
[docs] def contour3d_to_2d(self, contour3d, return_primitives_mapping: bool = False): """ Transforms a Contour3D into a Contour2D in the parametric domain of the surface. :param contour3d: The contour to be transformed. :type contour3d: :class:`wires.Contour3D` :param return_primitives_mapping: If True, returns a dictionary containing the correspondence between 2D and 3D primitives :type return_primitives_mapping: bool :return: A 2D contour object. :rtype: :class:`wires.Contour2D` """ primitives2d = [] primitives_mapping = {} # Transform the contour's primitives to parametric domain for primitive3d in contour3d.primitives: primitive3d = primitive3d.simplify if primitive3d.simplify.__class__.__name__ != "LineSegment3D" else \ primitive3d method_name = f'{primitive3d.__class__.__name__.lower()}_to_2d' if hasattr(self, method_name): primitives = getattr(self, method_name)(primitive3d) if primitives is None: continue self.update_primitives_mapping(primitives_mapping, primitives, primitive3d) primitives2d.extend(primitives) else: raise NotImplementedError( f'Class {self.__class__.__name__} does not implement {method_name}') contour2d = wires.Contour2D(primitives2d) if contour2d.is_ordered(1e-2): if return_primitives_mapping: return contour2d, primitives_mapping return contour2d self.repair_primitives_periodicity(primitives2d, primitives_mapping) if return_primitives_mapping: return wires.Contour2D(primitives2d), primitives_mapping return wires.Contour2D(primitives2d)
[docs] def is_lat_long_curve(self, arc): """ Checks if a curve defined on the sphere is a latitude/longitude curve. Returns True if it is, False otherwise. """ # Check if curve is a longitude curve (phi is constant) if self.frame.w.is_colinear_to(arc.circle.normal, abs_tol=1e-4): return True # Check if curve is a latitude curve (theta is constant) if self.frame.w.is_perpendicular_to(arc.circle.normal, abs_tol=1e-4) and \ arc.circle.center.is_close(self.frame.origin, 1e-4): return True return False
def _arc_start_end_3d_to_2d(self, arc3d): """ Helper function to fix periodicity issues while performing transformations into parametric domain. """ start = self.point3d_to_2d(arc3d.start) end = self.point3d_to_2d(arc3d.end) theta_i, _ = self.point3d_to_2d(arc3d.middle_point()) theta1, phi1 = start theta2, phi2 = end point_after_start, point_before_end = self._reference_points(arc3d) theta3, _ = point_after_start theta4, _ = point_before_end # Fix sphere singularity point if math.isclose(abs(phi1), 0.5 * math.pi, abs_tol=1e-2) and theta1 == 0.0 \ and math.isclose(theta3, theta_i, abs_tol=1e-2) and math.isclose(theta4, theta_i, abs_tol=1e-2): theta1 = theta_i start = design3d.Point2D(theta1, phi1) if math.isclose(abs(phi2), 0.5 * math.pi, abs_tol=1e-2) and theta2 == 0.0 \ and math.isclose(theta3, theta_i, abs_tol=1e-2) and math.isclose(theta4, theta_i, abs_tol=1e-2): theta2 = theta_i end = design3d.Point2D(theta2, phi2) discontinuity, _, _ = self._helper_arc3d_to_2d_periodicity_verifications(arc3d, start) start, end = d3d_parametric.arc3d_to_spherical_coordinates_verification( [start, end], [point_after_start, point_before_end], discontinuity) return start, end
[docs] def edge_passes_on_singularity_point(self, edge): """Helper function to verify id edge passes on the sphere singularity point.""" half_pi = 0.5 * math.pi point_positive_singularity = self.point2d_to_3d(design3d.Point2D(0, half_pi)) point_negative_singularity = self.point2d_to_3d(design3d.Point2D(0, -half_pi)) positive_singularity = edge.point_belongs(point_positive_singularity, 1e-6) negative_singularity = edge.point_belongs(point_negative_singularity, 1e-6) if positive_singularity and negative_singularity: return [point_positive_singularity, point_negative_singularity] if positive_singularity: return [point_positive_singularity, None] if negative_singularity: return [None, point_negative_singularity] return [None, None]
[docs] def arc3d_to_2d(self, arc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ is_lat_long_curve = self.is_lat_long_curve(arc3d) if is_lat_long_curve: start, end = self._arc_start_end_3d_to_2d(arc3d) singularity_points = self.edge_passes_on_singularity_point(arc3d) if any(singularity_points): return self.arc3d_to_2d_with_singularity(arc3d, start, end, singularity_points) return [edges.LineSegment2D(start, end)] return self.arc3d_to_2d_any_direction(arc3d)
[docs] def helper_arc3d_to_2d_with_singularity(self, arc3d, start, end, point_singularity, half_pi): """Helper function to arc3d_to_2d_with_singularity.""" theta1, phi1 = start theta2, phi2 = end if arc3d.is_point_edge_extremity(point_singularity): return [edges.LineSegment2D(start, end)] if math.isclose(abs(theta2 - theta1), math.pi, abs_tol=1e-2): if theta1 == math.pi and theta2 != math.pi: theta1 = -math.pi if theta2 == math.pi and theta1 != math.pi: theta2 = -math.pi primitives = [edges.LineSegment2D(design3d.Point2D(theta1, phi1), design3d.Point2D(theta1, half_pi)), edges.LineSegment2D(design3d.Point2D(theta1, half_pi), design3d.Point2D(theta2, half_pi), name="construction"), edges.LineSegment2D( design3d.Point2D( theta2, half_pi), design3d.Point2D( theta2, phi2)) ] return primitives n = 20 degree = 2 points = [self.point3d_to_2d(point3d) for point3d in arc3d.discretization_points(number_points=n)] return [edges.BSplineCurve2D.from_points_interpolation(points, degree)]
[docs] def arc3d_to_2d_with_singularity(self, arc3d, start, end, singularity_points): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ # trying to treat when the arc starts at theta1 passes at the singularity at |phi| = 0.5*math.pi # and ends at theta2 = theta1 + math.pi theta1, phi1 = start theta2, phi2 = end half_pi = 0.5 * math.pi point_positive_singularity, point_negative_singularity = singularity_points if point_positive_singularity and point_negative_singularity: if arc3d.is_point_edge_extremity(point_positive_singularity) and \ arc3d.is_point_edge_extremity(point_negative_singularity): return [edges.LineSegment2D(start, end)] direction_vector = arc3d.direction_vector(0) dot = self.frame.w.dot(direction_vector) if dot == 0: direction_vector = arc3d.direction_vector(0.01 * arc3d.length()) dot = self.frame.w.dot(direction_vector) if dot > 0: half_pi = 0.5 * math.pi thetai = theta1 - math.pi else: half_pi = -0.5 * math.pi thetai = theta1 + math.pi if arc3d.is_point_edge_extremity(point_positive_singularity): return [ edges.LineSegment2D(start, design3d.Point2D(start.x, -0.5 * math.pi)), edges.LineSegment2D(design3d.Point2D(start.x, -0.5 * math.pi), design3d.Point2D(theta2, -0.5 * math.pi), name="construction"), edges.LineSegment2D(design3d.Point2D(theta2, -0.5 * math.pi), design3d.Point2D(theta2, phi2)) ] if arc3d.is_point_edge_extremity(point_negative_singularity): return [ edges.LineSegment2D(start, design3d.Point2D(start.x, 0.5 * math.pi)), edges.LineSegment2D(design3d.Point2D(start.x, 0.5 * math.pi), design3d.Point2D(theta2, 0.5 * math.pi), name="construction"), edges.LineSegment2D(design3d.Point2D(theta2, 0.5 * math.pi), design3d.Point2D(theta2, phi2)) ] return [edges.LineSegment2D(design3d.Point2D(theta1, phi1), design3d.Point2D(theta1, half_pi)), edges.LineSegment2D(design3d.Point2D(theta1, half_pi), design3d.Point2D(thetai, half_pi), name="construction"), edges.LineSegment2D(design3d.Point2D(thetai, half_pi), design3d.Point2D(thetai, -half_pi)), edges.LineSegment2D(design3d.Point2D(thetai, -half_pi), design3d.Point2D(theta2, -half_pi), name="construction"), edges.LineSegment2D(design3d.Point2D(theta2, -half_pi), design3d.Point2D(theta2, phi2)) ] if point_positive_singularity: return self.helper_arc3d_to_2d_with_singularity(arc3d, start, end, point_positive_singularity, half_pi) if point_negative_singularity: return self.helper_arc3d_to_2d_with_singularity(arc3d, start, end, point_negative_singularity, -half_pi) raise NotImplementedError
@staticmethod def _fix_start_end_singularity_point_at_parametric_domain(edge, reference_point, point_at_singularity): """Uses tangent line to find real theta angle of the singularity point on parametric domain.""" _, phi = point_at_singularity abscissa_before_singularity = edge.abscissa(reference_point) direction_vector = edge.direction_vector(abscissa_before_singularity) direction_line = curves.Line2D(reference_point, reference_point + direction_vector) if phi > 0: line_positive_singularity = curves.Line2D(design3d.Point2D(-math.pi, 0.5 * math.pi), design3d.Point2D(math.pi, 0.5 * math.pi)) intersections = direction_line.line_intersections(line_positive_singularity) if intersections: return intersections[0] return intersections line_negative_singularity = curves.Line2D(design3d.Point2D(-math.pi, -0.5 * math.pi), design3d.Point2D(math.pi, -0.5 * math.pi)) intersections = direction_line.line_intersections(line_negative_singularity) if intersections: return intersections[0] return intersections
[docs] def is_point2d_on_sphere_singularity(self, point2d, tol=1e-5): """Verifies if point is on the spherical singularity point on parametric domain.""" half_pi = 0.5 * math.pi point = self.point2d_to_3d(point2d) point_positive_singularity = self.point2d_to_3d(design3d.Point2D(0, half_pi)) point_negative_singularity = self.point2d_to_3d(design3d.Point2D(0, -half_pi)) if point.is_close(point_positive_singularity, tol) or point.is_close(point_negative_singularity, tol): return True return False
[docs] def is_point3d_on_sphere_singularity(self, point3d): """Verifies if point is on the spherical singularity point on parametric domain.""" half_pi = 0.5 * math.pi point_positive_singularity = self.point2d_to_3d(design3d.Point2D(0, half_pi)) point_negative_singularity = self.point2d_to_3d(design3d.Point2D(0, -half_pi)) if point3d.is_close(point_positive_singularity) or point3d.is_close(point_negative_singularity): return True return False
[docs] def find_edge_start_end_undefined_parametric_points(self, edge3d, points, points3d): """ Helper function. Uses local discretization and line intersection with the tangent line at the point just before the undefined point on the BREP of the 3D edge to find the real value of theta on the sphere parametric domain. """ def get_temp_edge2d(_points): if len(_points) == 2: edge2d = edges.LineSegment2D(_points[0], _points[1]) else: edge2d = edges.BSplineCurve2D.from_points_interpolation(_points, 2) return edge2d if self.is_point3d_on_sphere_singularity(points3d[0]): distance = points3d[0].point_distance(points3d[1]) maximum_linear_distance_reference_point = 1e-5 if distance < maximum_linear_distance_reference_point: temp_points = points[1:] else: number_points = max(2, int(distance / maximum_linear_distance_reference_point)) local_discretization = [self.point3d_to_2d(point) for point in edge3d.local_discretization( points3d[0], points3d[1], number_points)] temp_points = local_discretization[1:] + points[2:] theta_list = [point.x for point in temp_points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: temp_points = self._fix_angle_discontinuity_on_discretization_points(temp_points, indexes_theta_discontinuity, "x") edge = get_temp_edge2d(temp_points) point = self._fix_start_end_singularity_point_at_parametric_domain(edge, reference_point=temp_points[1], point_at_singularity=points[0]) if point: points[0] = point else: per = 0.001 while per < 0.05: point = self.point3d_to_2d(edge3d.point_at_abscissa(per * edge3d.length())) if point != points[0]: break per += 0.0025 points[0] = point if self.is_point3d_on_sphere_singularity(points3d[-1]): distance = points3d[-2].point_distance(points3d[-1]) maximum_linear_distance_reference_point = 1e-5 if distance < maximum_linear_distance_reference_point: temp_points = points[:-1] else: number_points = max(2, int(distance / maximum_linear_distance_reference_point)) local_discretization = [self.point3d_to_2d(point) for point in edge3d.local_discretization( points3d[-2], points3d[-1], number_points)] temp_points = points[:-2] + local_discretization[:-1] theta_list = [point.x for point in temp_points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: temp_points = self._fix_angle_discontinuity_on_discretization_points( temp_points, indexes_theta_discontinuity, "x") edge = get_temp_edge2d(temp_points) point = self._fix_start_end_singularity_point_at_parametric_domain( edge, reference_point=temp_points[-2], point_at_singularity=points[-1]) if point: points[-1] = point else: per = 0.999 while per > 0.95: point = self.point3d_to_2d(edge3d.point_at_abscissa(per * edge3d.length())) if point != points[-1]: break per -= 0.0025 points[-1] = point return points
[docs] def arc3d_to_2d_any_direction_singularity(self, arc3d, point_singularity, half_pi): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ split = arc3d.split(point_singularity) primitive0 = self.arc3d_to_2d_any_direction(split[0])[0] primitive2 = self.arc3d_to_2d_any_direction(split[1])[0] primitive1 = edges.LineSegment2D(design3d.Point2D(primitive0.end.x, half_pi), design3d.Point2D(primitive2.start.x, half_pi)) return [primitive0, primitive1, primitive2]
[docs] def arc3d_to_2d_any_direction(self, arc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ singularity_points = self.edge_passes_on_singularity_point(arc3d) half_pi = 0.5 * math.pi # this variable avoid doing this multiplication several times (performance) point_positive_singularity, point_negative_singularity = singularity_points if point_positive_singularity and point_negative_singularity: raise ValueError("Impossible. This case should be treated by arc3d_to_2d_with_singularity method." "See arc3d_to_2d method for detail.") if point_positive_singularity and not arc3d.is_point_edge_extremity(point_positive_singularity): return self.arc3d_to_2d_any_direction_singularity(arc3d, point_positive_singularity, half_pi) if point_negative_singularity and not arc3d.is_point_edge_extremity(point_negative_singularity): return self.arc3d_to_2d_any_direction_singularity(arc3d, point_negative_singularity, -half_pi) number_points = max(math.ceil(arc3d.angle * 50) + 1, 5) points3d = arc3d.discretization_points(number_points=number_points) points = [self.point3d_to_2d(p) for p in points3d] point_after_start, point_before_end = self._reference_points(arc3d) start, end = d3d_parametric.spherical_repair_start_end_angle_periodicity( points[0], points[-1], point_after_start, point_before_end) points[0] = start points[-1] = end points = self.find_edge_start_end_undefined_parametric_points(arc3d, points, points3d) theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity([point.x for point in points]) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") return [edges.BSplineCurve2D.from_points_interpolation(points, 2)]
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ n = bspline_curve3d.ctrlpts.shape[0] points3d = bspline_curve3d.discretization_points(number_points=n) points = [self.point3d_to_2d(point) for point in points3d] point_after_start, point_before_end = self._reference_points(bspline_curve3d) start, end = d3d_parametric.spherical_repair_start_end_angle_periodicity( points[0], points[-1], point_after_start, point_before_end) points[0] = start points[-1] = end if start.x == 0.0 or end.x == 0.0: points = self.find_edge_start_end_undefined_parametric_points(bspline_curve3d, points, points3d) theta_list = [point.x for point in points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") degree = bspline_curve3d.degree if degree > len(points) - 1: degree = len(points) - 1 return [edges.BSplineCurve2D.from_points_interpolation(points, degree=degree).simplify]
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Converts a BREP BSpline curve 2D onto a 3D primitive on the surface. """ # TODO: this is incomplete, a bspline_curve2d can be also a bspline_curve3d i = round(0.5 * len(bspline_curve2d.points)) start = self.point2d_to_3d(bspline_curve2d.points[0]) interior = self.point2d_to_3d(bspline_curve2d.points[i]) end = self.point2d_to_3d(bspline_curve2d.points[-1]) vector_u1 = interior - start vector_u2 = interior - end points3d = [self.point2d_to_3d(p) for p in bspline_curve2d.points] if vector_u1.cross(vector_u2).norm(): arc3d = edges.Arc3D.from_3_points(start, interior, end) flag = True for point in points3d: if not arc3d.point_belongs(point, 1e-4): flag = False break if flag: return [arc3d] return [edges.BSplineCurve3D.from_points_interpolation(points3d, degree=bspline_curve2d.degree, centripetal=True)]
[docs] def arc2d_to_3d(self, arc2d): """ Converts a BREP arc 2D onto a 3D primitive on the surface. """ n = 10 degree = 2 points = [self.point2d_to_3d(point2d) for point2d in arc2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, degree).simplify]
@staticmethod def _horizontal_fullarc3d_to_2d(theta1, theta3, phi1, phi2): """ Helper Convert primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ point1 = design3d.Point2D(theta1, phi1) if theta1 > theta3: point2 = design3d.Point2D(theta1 - design3d.TWO_PI, phi2) elif theta1 < theta3: point2 = design3d.Point2D(theta1 + design3d.TWO_PI, phi2) return [edges.LineSegment2D(point1, point2)] @staticmethod def _vertical_through_origin_fullarc3d_to_2d(theta1, theta3, theta4, phi1, phi2, phi3): """ Helper Convert primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ if theta1 > theta3: theta_plus_pi = theta1 - math.pi else: theta_plus_pi = theta1 + math.pi if phi1 > phi3: half_pi = 0.5 * math.pi else: half_pi = -0.5 * math.pi if abs(phi1) == 0.5 * math.pi: return [edges.LineSegment2D(design3d.Point2D(theta3, phi1), design3d.Point2D(theta3, -half_pi)), edges.LineSegment2D(design3d.Point2D(theta4, -half_pi), design3d.Point2D(theta4, phi2))] return [edges.LineSegment2D(design3d.Point2D(theta1, phi1), design3d.Point2D(theta1, -half_pi)), edges.LineSegment2D(design3d.Point2D(theta_plus_pi, -half_pi), design3d.Point2D(theta_plus_pi, half_pi)), edges.LineSegment2D(design3d.Point2D(theta1, half_pi), design3d.Point2D(theta1, phi2))]
[docs] def fullarc3d_to_2d(self, fullarc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ # TODO: On a spherical surface we can have fullarc3d in any plane start, end = self._arc_start_end_3d_to_2d(fullarc3d) theta1, phi1 = start theta2, phi2 = end point_after_start, point_before_end = self._reference_points(fullarc3d) theta3, phi3 = point_after_start theta4, _ = point_before_end if self.frame.w.is_colinear_to(fullarc3d.circle.normal, abs_tol=1e-4): return self._horizontal_fullarc3d_to_2d(theta1, theta3, phi1, phi2) if self.frame.w.is_perpendicular_to(fullarc3d.circle.normal, abs_tol=1e-4) and \ self.frame.origin.is_close(fullarc3d.center): return self._vertical_through_origin_fullarc3d_to_2d(theta1, theta3, theta4, phi1, phi2, phi3) points = [self.point3d_to_2d(p) for p in fullarc3d.discretization_points(angle_resolution=25)] # Verify if theta1 or theta2 point should be -pi because atan2() -> ]-pi, pi] theta1 = d3d_parametric.repair_start_end_angle_periodicity(theta1, theta3) theta2 = d3d_parametric.repair_start_end_angle_periodicity(theta2, theta4) points[0] = design3d.Point2D(theta1, phi1) points[-1] = design3d.Point2D(theta2, phi2) theta_list = [point.x for point in points] theta_discontinuity, indexes_theta_discontinuity = angle_discontinuity(theta_list) if theta_discontinuity: points = self._fix_angle_discontinuity_on_discretization_points(points, indexes_theta_discontinuity, "x") return [edges.BSplineCurve2D.from_points_interpolation(points, 2)]
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), **kwargs): """Plot sphere arcs.""" if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') self.frame.plot(ax=ax, ratio=self.radius) for circle in self._circle_generatrices(50) + self._circle_generatrices_xy(50): circle.plot(ax, edge_style) return ax
[docs] def rectangular_cut(self, theta1, theta2, phi1, phi2, name=''): """Deprecated method, Use ShericalFace3D from_surface_rectangular_cut method.""" raise AttributeError('Use ShericalFace3D from_surface_rectangular_cut method')
[docs] def triangulation(self): """ Triangulation of Spherical Surface. """ face = self.rectangular_cut(0, design3d.TWO_PI, -0.5 * math.pi, 0.5 * math.pi) return face.triangulation()
[docs] def check_parametric_contour_end(self, primitives2d, tol): """Helper function to repair_primitives_periodicity.""" last_end = primitives2d[-1].end first_start = primitives2d[0].start if not last_end.is_close(first_start, tol=tol): last_end_3d = self.point2d_to_3d(last_end) first_start_3d = self.point2d_to_3d(first_start) if last_end_3d.is_close(first_start_3d, 1e-6) and not self.is_singularity_point(last_end_3d): if first_start.x > last_end.x: half_pi = -0.5 * math.pi else: half_pi = 0.5 * math.pi if not first_start.is_close(design3d.Point2D(first_start.x, half_pi)): lines = [edges.LineSegment2D( last_end, design3d.Point2D(last_end.x, half_pi), name="construction"), edges.LineSegment2D(design3d.Point2D(last_end.x, half_pi), design3d.Point2D(first_start.x, half_pi), name="construction"), edges.LineSegment2D(design3d.Point2D(first_start.x, half_pi), first_start, name="construction")] primitives2d.extend(lines) else: primitives2d.append(edges.LineSegment2D(last_end, first_start, name="construction"))
[docs] def is_singularity_point(self, point, *args, **kwargs): """Verifies if point is on the surface singularity.""" tol = kwargs.get("tol", 1e-6) positive_singularity = self.point2d_to_3d(design3d.Point2D(0.0, 0.5 * math.pi)) negative_singularity = self.point2d_to_3d(design3d.Point2D(0.0, -0.5 * math.pi)) return bool(positive_singularity.is_close(point, tol) or negative_singularity.is_close(point, tol))
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ Spherical Surface 3D rotation. :param center: rotation center :param axis: rotation axis :param angle: angle rotation :return: a new rotated Spherical Surface 3D """ new_frame = self.frame.rotation(center=center, axis=axis, angle=angle) return SphericalSurface3D(new_frame, self.radius)
[docs] def translation(self, offset: design3d.Vector3D): """ Spherical Surface 3D translation. :param offset: translation vector :return: A new translated Spherical Surface 3D """ new_frame = self.frame.translation(offset) return SphericalSurface3D(new_frame, self.radius)
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes Spherical Surface 3D's frame and return a new Spherical Surface 3D. :param frame: Frame of reference :type frame: `design3d.Frame3D` :param side: 'old' or 'new' """ new_frame = self.frame.frame_mapping(frame, side) return SphericalSurface3D(new_frame, self.radius)
[docs] def plane_intersections(self, plane3d): """ Sphere intersections with a plane. :param plane3d: intersecting plane. :return: list of intersecting curves. """ dist = plane3d.point_distance(self.frame.origin) if dist > self.radius: return [] if dist == self.radius: line = curves.Line3D(self.frame.origin, self.frame.origin + plane3d.frame.w) return plane3d.line_intersections(line) line = curves.Line3D(self.frame.origin, self.frame.origin + plane3d.frame.w) circle_radius = math.sqrt(self.radius ** 2 - dist ** 2) circle_center = plane3d.line_intersections(line)[0] start_end = circle_center + plane3d.frame.u * circle_radius circle = curves.Circle3D(design3d.Frame3D(circle_center, plane3d.frame.u, plane3d.frame.v, plane3d.frame.w), circle_radius) return [edges.FullArc3D(circle, start_end)]
[docs] def line_intersections(self, line: curves.Line3D): """ Calculates the intersection points between a 3D line and a spherical surface. The method calculates the intersection points between a 3D line and a sphere using the equation of the line and the equation of the sphere. It returns a list of intersection points, which can be empty if there are no intersections. The intersection points are represented as 3D points using the `design3d.Point3D` class. :param line: The 3D line object to intersect with the sphere. :type line:curves.Line3D :return: A list of intersection points between the line and the sphere. The list may be empty if there are no intersections. :rtype: List[design3d.Point3D] :Example: >>> from design3d import Point3D, edges, surfaces, OXYZ >>> spherical_surface3d = SphericalSurface3D(OXYZ, 1) >>> line2 = curves.Line3D(Point3D(0, 1, -0.5), Point3D(0, 1, 0.5)) >>> line_intersections2 = spherical_surface3d.line_intersections(line2) #returns [Point3D(0.0, 1.0, 0.0)] """ line_direction_vector = line.direction_vector() vector_linept1_center = self.frame.origin - line.point1 vector_linept1_center = vector_linept1_center.to_vector() a_param = line_direction_vector[0] ** 2 + line_direction_vector[1] ** 2 + line_direction_vector[2] ** 2 b_param = -2 * (line_direction_vector[0] * vector_linept1_center[0] + line_direction_vector[1] * vector_linept1_center[1] + line_direction_vector[2] * vector_linept1_center[2]) c_param = (vector_linept1_center[0] ** 2 + vector_linept1_center[1] ** 2 + vector_linept1_center[2] ** 2 - self.radius ** 2) b2_minus4ac = b_param ** 2 - 4 * a_param * c_param if math.isclose(b2_minus4ac, 0, abs_tol=1e-8): t_param = -b_param / (2 * a_param) return [line.point1 + line_direction_vector * t_param] if b2_minus4ac < 0: return [] t_param1 = (-b_param + math.sqrt(b2_minus4ac)) / (2 * a_param) t_param2 = (-b_param - math.sqrt(b2_minus4ac)) / (2 * a_param) return line.point1 + line_direction_vector * t_param1, line.point1 + line_direction_vector * t_param2
[docs] def circle_intersections(self, circle: curves.Circle3D): """ Gets intersections between a circle 3D and a SphericalSurface3D. :param circle: other circle to search intersections with. :return: list containing the intersection points. """ circle_plane = Plane3D(circle.frame) if circle_plane.point_distance(self.frame.origin) > self.radius: return [] circle_plane_intersections = self.plane_intersections(circle_plane) if circle_plane_intersections and isinstance(circle_plane_intersections[0], design3d.Point3D): return [] intersections = circle_plane_intersections[0].circle.circle_intersections(circle) return intersections
[docs] def arc_intersections(self, arc: edges.Arc3D): """ Gets intersections between an arc 3D and a SphericalSurface3D. :param arc: other arc to search intersections with. :return: list containing the intersection points. """ circle_intersections = self.circle_intersections(arc.circle) intersections = [intersection for intersection in circle_intersections if arc.point_belongs(intersection)] return intersections
[docs] def fullarc_intersections(self, fullarc: edges.Arc3D): """ Gets intersections between a fullarc 3D and a SphericalSurface3D. :param fullarc: other fullarc to search intersections with. :return: list containing the intersection points. """ return self.circle_intersections(fullarc.circle)
[docs] def ellipse_intersections(self, ellipse: curves.Ellipse3D): """ Gets intersections between an ellipse 3D and a SphericalSurface3D. :param ellipse: other ellipse to search intersections with. :return: list containing the intersection points. """ ellipse_plane = Plane3D(ellipse.frame) if ellipse_plane.point_distance(self.frame.origin) > self.radius: return [] ellipse_plane_intersections = self.plane_intersections(ellipse_plane) intersections = ellipse_plane_intersections[0].circle.ellipse_intersections(ellipse) return intersections
[docs] def arcellipse_intersections(self, arcellipse: edges.ArcEllipse3D): """ Gets intersections between an arcellipse 3D and a SphericalSurface3D. :param arcellipse: other arcellipse to search intersections with. :return: list containing the intersection points. """ circle_intersections = self.ellipse_intersections(arcellipse.ellipse) intersections = [intersection for intersection in circle_intersections if arcellipse.point_belongs(intersection)] return intersections
[docs] def fullarcellipse_intersections(self, fullarcellipse: edges.FullArcEllipse3D): """ Gets intersections between a full arcellipse 3D and a SphericalSurface3D. :param fullarcellipse: other full arcellipse to search intersections with. :return: list containing the intersection points. """ return self.ellipse_intersections(fullarcellipse.ellipse)
def _spherical_intersection_points(self, spherical_surface: 'SphericalSurface3D'): """ Gets the points of intersections between the spherical surface and the toroidal surface. :param spherical_surface: other Spherical Surface 3d. :return: points of intersections. """ cyl_generatrices = self._circle_generatrices(200) + self._circle_generatrices_xy(200) intersection_points = [] for gene in cyl_generatrices: intersections = spherical_surface.edge_intersections(gene) for intersection in intersections: if not intersection.in_list(intersection_points): intersection_points.append(intersection) return intersection_points
[docs] def sphericalsurface_intersections(self, spherical_surface: 'SphericalSurface3D'): """ Cylinder Surface intersections with a Spherical surface. :param spherical_surface: intersecting sphere. :return: list of intersecting curves. """ intersection_points = self._spherical_intersection_points(spherical_surface) if not intersection_points: return [] inters_points = d3d_common_operations.separate_points_by_closeness(intersection_points) curves_ = [] for list_points in inters_points: bspline = edges.BSplineCurve3D.from_points_interpolation(list_points, 4, centripetal=False) if isinstance(bspline.simplify, edges.FullArc3D): curves_.append(bspline.simplify) continue curves_.append(bspline) return curves_
[docs] def u_iso(self, u: float) -> curves.Circle3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A circle 3D :rtype: :class:`curves.Circle3D` """ center = self.frame.origin point_at_u_v0 = self.point2d_to_3d(design3d.Point2D(u, 0.0)) u_vector = (point_at_u_v0 - center).unit_vector() frame = design3d.Frame3D(center, u_vector, self.frame.w, u_vector.cross(self.frame.w)) return curves.Circle3D(frame, self.radius)
[docs] def v_iso(self, v: float) -> curves.Circle3D: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A Circle 3D :rtype: :class:`curves.Circle3D` """ radius = self.radius * math.cos(v) if radius < 1e-15: return None z = self.radius * math.sin(v) frame = self.frame.translation(self.frame.w * z) return curves.Circle3D(frame, radius)
[docs] def normal_at_point(self, point: design3d.Point3D): """ Gets normal vector at given point on the surface. :param point: point to be verified. :return: normal """ if not self.point_belongs(point): raise ValueError('Point given not on surface.') theta, phi = self.point3d_to_2d(point) normal = math.cos(phi) * (math.cos(theta) * self.frame.u + math.sin(theta) * self.frame.v) + math.sin(theta) * self.frame.w return normal
[docs] class RuledSurface3D(Surface3D): """ Defines a ruled surface between two wires. :param wire1: Wire :type wire1: :class:`d3dw.Wire3D` :param wire2: Wire :type wire2: :class:`wires.Wire3D` """ face_class = 'RuledFace3D' def __init__(self, wire1: wires.Wire3D, wire2: wires.Wire3D, name: str = ''): self.wire1 = wire1 self.wire2 = wire2 self.length1 = wire1.length() self.length2 = wire2.length() Surface3D.__init__(self, name=name) def __hash__(self): return hash((self.__class__.__name__, self.wire1, self.wire2)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.wire1 == other.wire1 and self.wire2 == other.wire2: return True return False
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Coverts a parametric coordinate on the surface into a 3D spatial point (x, y, z). :param point2d: Point at the ToroidalSuface3D :type point2d: `design3d.`Point2D` """ x, y = point2d point1 = self.wire1.point_at_abscissa(x * self.length1) point2 = self.wire2.point_at_abscissa(x * self.length2) joining_line = edges.LineSegment3D(point1, point2) point = joining_line.point_at_abscissa(y * joining_line.length()) return point
[docs] def point3d_to_2d(self, point3d): """ Returns the parametric coordinates design3d.Point2D(u, v) of a cartesian coordinates point (x, y, z). :param point3d: Point at the CylindricalSuface3D :type point3d: `design3d.`Point3D` """ raise NotImplementedError
[docs] def rectangular_cut(self, x1: float, x2: float, y1: float, y2: float, name: str = ''): """Deprecated method, Use RuledFace3D from_surface_rectangular_cut method.""" raise NotImplementedError('Use RuledFace3D from_surface_rectangular_cut method')
[docs] class ExtrusionSurface3D(Surface3D): """ Defines a surface of extrusion. An extrusion surface is a surface that is a generic cylindrical surface generated by the linear extrusion of a curve, generally an Ellipse or a B-Spline curve. :param edge: edge. :type edge: Union[:class:`d3dw.Wire3D`, :class:`d3dw.Contour3D`] :param axis_point: Axis placement :type axis_point: :class:`design3d.Point3D` :param axis: Axis of extrusion :type axis: :class:`design3d.Vector3D` """ face_class = 'ExtrusionFace3D' y_periodicity = None def __init__(self, edge: Union[edges.FullArcEllipse3D, edges.BSplineCurve3D], direction: design3d.Vector3D, name: str = ''): self.edge = edge direction = direction.unit_vector() self.direction = direction if hasattr(edge, "center"): frame = design3d.Frame3D.from_point_and_vector(edge.center, direction, design3d.Z3D) else: frame = design3d.Frame3D.from_point_and_vector(edge.start, direction, design3d.Z3D) self._x_periodicity = False Surface3D.__init__(self, frame=frame, name=name) def __hash__(self): return hash((self.__class__.__name__, self.edge, self.direction)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.edge == other.edge and self.direction == other.direction: return True return False @property def x_periodicity(self): """Returns the periodicity in x direction.""" if self._x_periodicity: return self._x_periodicity start = self.edge.start end = self.edge.end if start.is_close(end, 1e-6): self._x_periodicity = self.edge.length() return self._x_periodicity return None @x_periodicity.setter def x_periodicity(self, value): """X periodicity setter.""" self._x_periodicity = value @property def u_domain(self): """The parametric domain of the surface in the U direction.""" return 0.0, self.edge.length() @property def v_domain(self): """The parametric domain of the surface in the V direction.""" return -math.inf, math.inf
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Transform a parametric (u, v) point into a 3D Cartesian point (x, y, z). # u = [0, 1] and v = z """ u, v = point2d if abs(u) < 1e-7: u = 0.0 if abs(v) < 1e-7: v = 0.0 if self.x_periodicity: if u > self.x_periodicity: u -= self.x_periodicity elif u < 0: u += self.x_periodicity point_at_curve = self.edge.point_at_abscissa(u) point = point_at_curve.translation(self.frame.w * v) return point
[docs] def point3d_to_2d(self, point3d): """ Transform a 3D Cartesian point (x, y, z) into a parametric (u, v) point. """ x, y, z = self.frame.global_to_local_coordinates(point3d) if abs(x) < 1e-7: x = 0.0 if abs(y) < 1e-7: y = 0.0 if abs(z) < 1e-7: z = 0.0 point_at_curve = [] tol = 1e-4 if self.edge.__class__.__name__ in ("FullArcEllipse3D", "ArcEllipse3D") else 1e-6 if hasattr(self.edge, "line_intersections"): line = curves.Line3D(point3d, point3d.translation(self.frame.w)) point_at_curve = self.edge.line_intersections(line, tol) if point_at_curve: point_at_curve = point_at_curve[0] point_at_curve_local = self.frame.global_to_local_coordinates(point_at_curve) else: if hasattr(self.edge, "point_projection"): point_at_curve = self.edge.point_projection(point3d)[0] point_at_curve_local = self.frame.global_to_local_coordinates(point_at_curve) else: point_at_curve_local = design3d.Point3D(x, y, 0) point_at_curve = self.frame.local_to_global_coordinates(point_at_curve_local) u = self.edge.abscissa(point_at_curve) v = z - point_at_curve_local.z return design3d.Point2D(u, v)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the extrusion surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the extrusion surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the extrusion surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ z = np.asarray([self.direction[0], self.direction[1], self.direction[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] if self.x_periodicity: u_values[u_values > self.x_periodicity] -= self.x_periodicity u_values[u_values < 0] += self.x_periodicity v_values = points[:, 1] points_at_curve = np.asarray([self.edge.point_at_abscissa(u) for u in u_values]) return points_at_curve + v_values * z
[docs] def rectangular_cut(self, x1: float = 0.0, x2: float = 1.0, y1: float = 0.0, y2: float = 1.0, name: str = ''): """Deprecated method, Use ExtrusionFace3D from_surface_rectangular_cut method.""" raise AttributeError('Use ExtrusionFace3D from_surface_rectangular_cut method')
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), z: float = 0.5, **kwargs): """Plot for extrusion surface using matplotlib.""" if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') self.frame.plot(ax=ax, ratio=self.edge.length()) for i in range(21): step = i / 20. * z wire = self.edge.translation(step * self.frame.w) wire.plot(ax=ax, edge_style=edge_style) for i in range(21): step = -i / 20. * z wire = self.edge.translation(step * self.frame.w) wire.plot(ax=ax, edge_style=edge_style) return ax
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """Creates an extrusion surface from step data.""" name = arguments[0][1:-1] edge = object_dict[arguments[1]] if edge.__class__ is curves.Ellipse3D: start_end = edge.center + edge.major_axis * edge.major_dir fullarcellipse = edges.FullArcEllipse3D(edge, start_end, edge.name) direction = -object_dict[arguments[2]] surface = cls(edge=fullarcellipse, direction=direction, name=name) surface.x_periodicity = fullarcellipse.length() elif edge.__class__ is curves.Circle3D: start_end = edge.center + edge.frame.u * edge.radius fullarc = edges.FullArc3D(edge, start_end) direction = object_dict[arguments[2]] surface = cls(edge=fullarc, direction=direction, name=name) surface.x_periodicity = fullarc.length() else: direction = object_dict[arguments[2]] surface = cls(edge=edge, direction=direction, name=name) return surface
[docs] def to_step(self, current_id): """ Translate design3d primitive to step syntax. """ content_edge, edge_id = self.edge.to_step(current_id) current_id = edge_id + 1 content_vector, vector_id = self.direction.to_step(current_id) current_id = vector_id + 1 content = content_edge + content_vector content += f"#{current_id} = SURFACE_OF_LINEAR_EXTRUSION('{self.name}',#{edge_id},#{vector_id});\n" return content, [current_id]
[docs] def linesegment3d_to_2d(self, linesegment3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(linesegment3d.start) end = self.point3d_to_2d(linesegment3d.end) if self.x_periodicity: line_at_periodicity = curves.Line3D(self.edge.start, self.edge.start.translation(self.direction)) if (line_at_periodicity.point_belongs(linesegment3d.start) and line_at_periodicity.point_belongs(linesegment3d.end) and start.x != end.x): end.x = start.x return [edges.LineSegment2D(start, end)]
[docs] def arc3d_to_2d(self, arc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(arc3d.start) end = self.point3d_to_2d(arc3d.end) if self.x_periodicity: start, end = self._verify_start_end_parametric_points(start, end, arc3d) points3d = [arc3d.start, arc3d.point_at_abscissa(0.02 * arc3d.length()), arc3d.point_at_abscissa(0.98 * arc3d.length()), arc3d.end] point_after_start = self.point3d_to_2d(points3d[1]) point_before_end = self.point3d_to_2d(points3d[2]) start, _, _, end = self._repair_points_order([start, point_after_start, point_before_end, end], arc3d, points3d) return [edges.LineSegment2D(start, end, name="arc")]
[docs] def arcellipse3d_to_2d(self, arcellipse3d): """ Transformation of an arc-ellipse 3d to 2d, in a cylindrical surface. """ start2d = self.point3d_to_2d(arcellipse3d.start) end2d = self.point3d_to_2d(arcellipse3d.end) if isinstance(self.edge, edges.ArcEllipse3D): return [edges.LineSegment2D(start2d, end2d)] points3d = arcellipse3d.discretization_points(number_points=15) points = [self.point3d_to_2d(p) for p in points3d] return self._edge3d_to_2d(points, arcellipse3d, points3d)
[docs] def fullarcellipse3d_to_2d(self, fullarcellipse3d): """ Converts a 3D full elliptical arc to a 2D line segment in the current plane. This method converts a 3D full elliptical arc to a 2D line segment in the current plane. It first calculates the length of the arc using the `length` method of the `fullarcellipse3d` object. Then, it converts the start and end points of the arc to 2D points using the `point3d_to_2d` method. Additionally, it calculates a point on the arc at a small abscissa value (0.01 * length) and converts it to a 2D point. Based on the relative position of this point, the method determines the start and end points of the line segment in 2D. If the abscissa point is closer to the start point, the line segment starts from (0, start.y) and ends at (length, end.y). If the abscissa point is closer to the end point, the line segment starts from (length, start.y) and ends at (0, end.y). If the abscissa point lies exactly at the midpoint of the arc, a NotImplementedError is raised. The resulting line segment is returned as a list. :param fullarcellipse3d: The 3D full elliptical arc object to convert. :return: A list containing a 2D line segment representing the converted arc. :raises: NotImplementedError: If the abscissa point lies exactly at the midpoint of the arc. """ length = fullarcellipse3d.length() start = self.point3d_to_2d(fullarcellipse3d.start) end = self.point3d_to_2d(fullarcellipse3d.end) u3, _ = self.point3d_to_2d(fullarcellipse3d.point_at_abscissa(0.01 * length)) if u3 > 0.5 * length: start.x = length end.x = 0.0 elif u3 < 0.5 * length: start.x = 0.0 end.x = length else: raise NotImplementedError return [edges.LineSegment2D(start, end)]
[docs] def linesegment2d_to_3d(self, linesegment2d): """ Converts a BREP line segment 2D onto a 3D primitive on the surface. """ start3d = self.point2d_to_3d(linesegment2d.start) end3d = self.point2d_to_3d(linesegment2d.end) u1, param_z1 = linesegment2d.start u2, param_z2 = linesegment2d.end if math.isclose(u1, u2, abs_tol=1e-6): return [edges.LineSegment3D(start3d, end3d)] if math.isclose(param_z1, param_z2, abs_tol=1e-6): curve = self.v_iso(param_z1) if u1 > u2: curve = curve.reverse() return [curve.trim(start3d, end3d)] n = 20 degree = 5 points = [self.point2d_to_3d(point2d) for point2d in linesegment2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, degree, centripetal=True)]
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ n = bspline_curve3d.ctrlpts.shape[0] points3d = bspline_curve3d.discretization_points(number_points=n) points = [self.point3d_to_2d(point) for point in points3d] return self._edge3d_to_2d(points, bspline_curve3d, points3d)
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Returns a new Extrusion Surface positioned in the specified frame. :param frame: Frame of reference :type frame: `design3d.Frame3D` :param side: 'old' or 'new' """ new_frame = self.frame.frame_mapping(frame, side) direction = new_frame.w new_edge = self.edge.frame_mapping(frame, side) return ExtrusionSurface3D(new_edge, direction, name=self.name)
def _verify_start_end_parametric_points(self, start, end, edge3d): """ When the generatrix of the surface is periodic we need to verify if the u parameter should be 0 or 1. """ start_ref1 = self.point3d_to_2d(edge3d.point_at_abscissa(0.01 * edge3d.length())) start_ref2 = self.point3d_to_2d(edge3d.point_at_abscissa(0.02 * edge3d.length())) end_ref1 = self.point3d_to_2d(edge3d.point_at_abscissa(0.99 * edge3d.length())) end_ref2 = self.point3d_to_2d(edge3d.point_at_abscissa(0.98 * edge3d.length())) if math.isclose(start.x, self.x_periodicity, abs_tol=1e-4): vec1 = start_ref1 - start vec2 = start_ref2 - start_ref1 if vec2.dot(vec1) < 0: start.x = 0 if math.isclose(end.x, self.x_periodicity, abs_tol=1e-4): vec1 = end - end_ref1 vec2 = end_ref1 - end_ref2 if vec2.dot(vec1) < 0: end.x = 0 if math.isclose(start.x, 0, abs_tol=1e-4): vec1 = start_ref1 - start vec2 = start_ref2 - start_ref1 if vec2.dot(vec1) < 0: start.x = self.x_periodicity if math.isclose(end.x, 0, abs_tol=1e-4): vec1 = end - end_ref1 vec2 = end_ref1 - end_ref2 if vec2.dot(vec1) < 0: end.x = self.x_periodicity return start, end def _repair_points_order(self, points: List[design3d.Point2D], edge3d, points3d: List[design3d.Point3D]) -> List[design3d.Point2D]: """ Helper function to reorder discretization points along a parametric domain for an extrusion surface. When generating an extrusion surface from a periodic edge, there may be discontinuities in the parametric representation of the edge on the surface. This function addresses this issue by calculating how many times the edge crosses the periodic boundary of the surface. It then selects the first side as reference, and then all parts of the edge on the "opposite" side (lower/upper bound) are updated by adding or subtracting one periodicity. This is achieved using the 'sign' variable. The result is a list of parametric points that form a continuous path in parametric space. The cache_point_index keeps track of points that were already checked, so we don't need to check it again in the next remaining edge's piece. :param points: List of 2D parametric points representing the discretization of the edge on the surface. :param edge3d: The 3D curve representing the edge. :param points3d: List of corresponding 3D points. :return: The reordered list of parametric points forming a continuous path on the extrusion surface. """ line_at_periodicity = curves.Line3D(self.edge.start, self.edge.start.translation(self.direction)) intersections = edge3d.line_intersections(line_at_periodicity) intersections = [point for point in intersections if not edge3d.is_point_edge_extremity(point, abs_tol=5e-6)] if not intersections: return points sign = self._helper_get_sign_repair_points_order(edge3d, points[0], intersections[0]) remaining_edge = edge3d cache_point_index = 0 crossed_even_number_of_times = True for i, intersection in enumerate(intersections): current_split = remaining_edge.split(intersection) crossed_even_number_of_times = bool(i % 2 == 0) for point, point3d in zip(points[cache_point_index:], points3d[cache_point_index:]): if crossed_even_number_of_times and current_split[0].point_belongs(point3d): cache_point_index += 1 elif not crossed_even_number_of_times and current_split[0].point_belongs(point3d): point.x = point.x + sign * self.x_periodicity cache_point_index += 1 remaining_edge = current_split[1] if crossed_even_number_of_times and cache_point_index < len(points): for point in points[cache_point_index:]: point.x = point.x + sign * self.x_periodicity return points def _helper_get_sign_repair_points_order(self, edge3d, starting_parametric_point, first_intersection_point): """Helper function to repair points order.""" reference_point = edge3d.local_discretization(edge3d.start, first_intersection_point, 3)[1] reference_point_u_parm = self.point3d_to_2d(reference_point).x diff = reference_point_u_parm - starting_parametric_point.x return diff / abs(diff) def _edge3d_to_2d(self, points, edge3d, points3d): """Helper to get parametric representation of edges on the surface.""" if self.x_periodicity: start, end = self._verify_start_end_parametric_points(points[0], points[-1], edge3d) points[0] = start points[-1] = end points = self._repair_points_order(points, edge3d, points3d) start = points[0] end = points[-1] if is_isocurve(points, 1e-5): return [edges.LineSegment2D(start, end)] if hasattr(edge3d, "degree"): degree = edge3d.degree else: degree = 2 return [edges.BSplineCurve2D.from_points_interpolation(points, degree)]
[docs] def u_iso(self, u: float) -> curves.Line3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A line 3D :rtype: :class:`curves.Line3D` """ point_at_u = self.point2d_to_3d(design3d.Point2D(u, 0.0)) return curves.Line3D.from_point_and_vector(point_at_u, self.frame.w)
[docs] def v_iso(self, v: float) -> curves.Curve: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A Curve :rtype: :class:`curves.Curve` """ return self.edge.curve().translation(self.direction * v)
[docs] class RevolutionSurface3D(UPeriodicalSurface): """ Defines a surface of revolution. :param edge: Edge. :type edge: edges.Edge :param axis_point: Axis placement :type axis_point: :class:`design3d.Point3D` :param axis: Axis of revolution :type axis: :class:`design3d.Vector3D` """ face_class = 'RevolutionFace3D' x_periodicity = design3d.TWO_PI def __init__(self, edge, axis_point: design3d.Point3D, axis: design3d.Vector3D, name: str = ''): self.edge = edge self.axis_point = axis_point self.axis = axis.unit_vector() point1 = edge.point_at_abscissa(0) vector1 = point1 - axis_point w_vector = self.axis if point1.is_close(axis_point) or w_vector.is_colinear_to(vector1): if edge.__class__.__name__ != "Line3D": point1 = edge.point_at_abscissa(0.5 * edge.length()) else: point1 = edge.point_at_abscissa(0.05) vector1 = point1 - axis_point u_vector = vector1 - vector1.vector_projection(w_vector) u_vector = u_vector.unit_vector() v_vector = w_vector.cross(u_vector) frame = design3d.Frame3D(origin=axis_point, u=u_vector, v=v_vector, w=w_vector) UPeriodicalSurface.__init__(self, frame=frame, name=name) def __hash__(self): return hash((self.__class__.__name__, self.edge, self.axis_point, self.axis)) def __eq__(self, other): if self.__class__.__name__ != other.__class__.__name__: return False if self.edge == other.edge and self.axis_point == other.axis_point and self.axis == other.axis: return True return False @property def y_periodicity(self): """ Evaluates the periodicity of the surface in v direction. """ a, b, c, d = self.domain point_at_c = self.point2d_to_3d(design3d.Point2D(0.5 * (b - a), c)) point_at_d = self.point2d_to_3d(design3d.Point2D(0.5 * (b - a), d)) if point_at_d.is_close(point_at_c): return d return None @property def u_domain(self): """The parametric domain of the surface in the U direction.""" return -math.pi, math.pi @property def v_domain(self): """The parametric domain of the surface in the V direction.""" if self.edge.__class__.__name__ != "Line3D": return 0.0, self.edge.length() return -math.inf, math.inf @property def domain(self): """Returns u and v bounds.""" d3din, d3dax = self.v_domain return -math.pi, math.pi, d3din, d3dax
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Transform a parametric (u, v) point into a 3D Cartesian point (x, y, z). u = [0, 2pi] and v = [0, 1] into a """ u, v = point2d point_at_curve = self.edge.point_at_abscissa(v) point_vector = point_at_curve - self.axis_point point3d = (self.axis_point + point_vector * math.cos(u) + point_vector.dot(self.axis) * self.axis * (1 - math.cos(u)) + self.axis.cross(point_vector) * math.sin(u)) return point3d
[docs] def point3d_to_2d(self, point3d): """ Transform a 3D Cartesian point (x, y, z) into a parametric (u, v) point. """ x, y, _ = self.frame.global_to_local_coordinates(point3d) if abs(x) < 1e-12: x = 0 if abs(y) < 1e-12: y = 0 u = math.atan2(y, x) point_at_curve = point3d.rotation(self.axis_point, self.axis, -u) v = self.edge.abscissa(point_at_curve) return design3d.Point2D(u, v)
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the revolution surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the revolution surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the revolution surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ center = np.asarray(self.axis_point) z = np.asarray([self.axis[0], self.axis[1], self.axis[2]]) points = points.reshape(-1, 2, 1) u_values = points[:, 0] v_values = points[:, 1] if self.y_periodicity: v_values[v_values > self.y_periodicity] -= self.y_periodicity v_values[v_values < 0] += self.y_periodicity cos_u = np.cos(u_values) points_at_curve = np.asarray([self.edge.point_at_abscissa(v) for v in v_values]) points_at_curve_minus_center = points_at_curve - center return (center + points_at_curve_minus_center * cos_u + np.dot(points_at_curve_minus_center, z).reshape(-1, 1) * z * (1 - cos_u) + np.cross(z, points_at_curve_minus_center * np.sin(u_values)))
[docs] def rectangular_cut(self, x1: float, x2: float, y1: float, y2: float, name: str = ''): """Deprecated method, Use RevolutionFace3D from_surface_rectangular_cut method.""" raise AttributeError('Use RevolutionFace3D from_surface_rectangular_cut method')
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), number_curves: int = 20, **kwargs): """ Plot rotated Revolution surface generatrix. :param number_curves: Number of curves to display. :param ax: matplotlib axis. :param edge_style: plot edge style. :type number_curves: int """ if ax is None: fig = plt.figure() ax = fig.add_subplot(111, projection='3d') for i in range(number_curves + 1): theta = i / number_curves * design3d.TWO_PI wire = self.edge.rotation(self.axis_point, self.axis, theta) wire.plot(ax=ax, edge_style=edge_style) return ax
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a RevolutionSurface3D. :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 RevolutionSurface3D object. :rtype: :class:`design3d.faces.RevolutionSurface3D` """ name = arguments[0][1:-1] edge = object_dict[arguments[1]] if edge.__class__ is curves.Circle3D: start_end = edge.center + edge.frame.u * edge.radius edge = edges.FullArc3D(edge, start_end, edge.name) axis_point, axis = object_dict[arguments[2]] surface = cls(edge=edge, axis_point=axis_point, axis=axis, name=name) return surface.simplify()
[docs] def to_step(self, current_id): """ Translate design3d primitive to step syntax. """ content_wire, wire_id = self.edge.to_step(current_id) current_id = wire_id + 1 content_axis_point, axis_point_id = self.axis_point.to_step(current_id) current_id = axis_point_id + 1 content_axis, axis_id = self.axis.to_step(current_id) current_id = axis_id + 1 content = content_wire + content_axis_point + content_axis content += f"#{current_id} = AXIS1_PLACEMENT('',#{axis_point_id},#{axis_id});\n" current_id += 1 content += f"#{current_id} = SURFACE_OF_REVOLUTION('{self.name}',#{wire_id},#{current_id - 1});\n" return content, [current_id]
[docs] def arc3d_to_2d(self, arc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(arc3d.start) end = self.point3d_to_2d(arc3d.end) if self.edge.__class__.__name__ != "Line3D" and hasattr(self.edge.simplify, "circle") and \ math.isclose(self.edge.simplify.circle.radius, arc3d.circle.radius, rel_tol=0.01): if self.edge.is_point_edge_extremity(arc3d.start): middle_point = self.point3d_to_2d(arc3d.middle_point()) if middle_point.x == math.pi: middle_point.x = -math.pi if end.x == math.pi: end.x = middle_point.x start.x = middle_point.x if self.edge.is_point_edge_extremity(arc3d.end): middle_point = self.point3d_to_2d(arc3d.middle_point()) if middle_point.x == math.pi: middle_point.x = -math.pi if start.x == math.pi: start.x = middle_point.x end.x = middle_point.x if math.isclose(start.y, end.y, rel_tol=0.01): point_after_start, point_before_end = self._reference_points(arc3d) point_theta_discontinuity = self.point2d_to_3d(design3d.Point2D(math.pi, start.y)) discontinuity = arc3d.point_belongs(point_theta_discontinuity) and not \ arc3d.is_point_edge_extremity(point_theta_discontinuity) undefined_start_theta = arc3d.start.is_close(point_theta_discontinuity) undefined_end_theta = arc3d.end.is_close(point_theta_discontinuity) start, end = d3d_parametric.arc3d_to_cylindrical_coordinates_verification( [start, end], [undefined_start_theta, undefined_end_theta], [point_after_start.x, point_before_end.x], discontinuity) if math.isclose(start.y, end.y, rel_tol=0.01) or math.isclose(start.x, end.x, rel_tol=0.01): return [edges.LineSegment2D(start, end, name="arc")] n = 10 degree = 3 bsplinecurve3d = edges.BSplineCurve3D.from_points_interpolation(arc3d.discretization_points(number_points=n), degree, centripetal=True) return self.bsplinecurve3d_to_2d(bsplinecurve3d)
[docs] def fullarc3d_to_2d(self, fullarc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ start = self.point3d_to_2d(fullarc3d.start) end = self.point3d_to_2d(fullarc3d.end) point_after_start, point_before_end = self._reference_points(fullarc3d) point_theta_discontinuity = self.point2d_to_3d(design3d.Point2D(math.pi, start.y)) discontinuity = fullarc3d.point_belongs(point_theta_discontinuity) and not \ fullarc3d.is_point_edge_extremity(point_theta_discontinuity) undefined_start_theta = fullarc3d.start.is_close(point_theta_discontinuity) undefined_end_theta = fullarc3d.end.is_close(point_theta_discontinuity) start, end = d3d_parametric.arc3d_to_cylindrical_coordinates_verification( [start, end], [undefined_start_theta, undefined_end_theta], [point_after_start.x, point_before_end.x], discontinuity) theta1, z1 = start theta2, _ = end _, z3 = point_after_start if self.frame.w.is_colinear_to(fullarc3d.circle.normal): normal_dot_product = self.frame.w.dot(fullarc3d.circle.normal) start, end = d3d_parametric.fullarc_to_cylindrical_coordinates_verification(start, end, normal_dot_product) return [edges.LineSegment2D(start, end)] if math.isclose(theta1, theta2, abs_tol=1e-3): # Treating one case from Revolution Surface if z1 > z3: point1 = design3d.Point2D(theta1, 1) point2 = design3d.Point2D(theta1, 0) else: point1 = design3d.Point2D(theta1, 0) point2 = design3d.Point2D(theta1, 1) return [edges.LineSegment2D(point1, point2)] if math.isclose(abs(theta1 - theta2), math.pi, abs_tol=1e-3): if z1 > z3: point1 = design3d.Point2D(theta1, 1) point2 = design3d.Point2D(theta1, 0) point3 = design3d.Point2D(theta2, 0) point4 = design3d.Point2D(theta2, 1) else: point1 = design3d.Point2D(theta1, 0) point2 = design3d.Point2D(theta1, 1) point3 = design3d.Point2D(theta2, 1) point4 = design3d.Point2D(theta2, 0) return [edges.LineSegment2D(point1, point2), edges.LineSegment2D(point2, point3), edges.LineSegment2D(point3, point4) ] raise NotImplementedError
[docs] def linesegment2d_to_3d(self, linesegment2d): """ Converts a BREP line segment 2D onto a 3D primitive on the surface. """ if linesegment2d.name == "construction" or self.is_degenerated_brep(linesegment2d): return None start3d = self.point2d_to_3d(linesegment2d.start) end3d = self.point2d_to_3d(linesegment2d.end) theta1, abscissa1 = linesegment2d.start theta2, abscissa2 = linesegment2d.end if self.edge.point_at_abscissa(abscissa1).is_close(self.edge.point_at_abscissa(abscissa2)): circle = self.v_iso(abscissa1) if theta1 > theta2: circle = circle.reverse() return [circle.trim(start3d, end3d)] if math.isclose(theta1, theta2, abs_tol=1e-3): curve = self.u_iso(theta1) if abscissa1 > abscissa2: curve = curve.reverse() return [curve.trim(start3d, end3d)] n = int(54 * abs(theta2 - theta1)/math.pi) degree = 7 points = [self.point2d_to_3d(point2d) for point2d in linesegment2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, degree, centripetal=True).simplify]
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Is this right?. """ n = len(bspline_curve2d.control_points) points = [self.point2d_to_3d(p) for p in bspline_curve2d.discretization_points(number_points=n)] return [edges.BSplineCurve3D.from_points_interpolation(points, bspline_curve2d.degree, centripetal=True)]
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Returns a new Revolution Surface positioned in the specified frame. :param frame: Frame of reference :type frame: `design3d.Frame3D` :param side: 'old' or 'new' """ new_frame = self.frame.frame_mapping(frame, side) axis = new_frame.w axis_point = new_frame.origin new_edge = self.edge.frame_mapping(frame, side) return RevolutionSurface3D(new_edge, axis_point, axis, name=self.name)
[docs] def translation(self, offset): """ Returns a new translated Revolution Surface. :param offset: translation vector. """ new_edge = self.edge.translation(offset) new_axis_point = self.axis_point.translation(offset) return RevolutionSurface3D(new_edge, new_axis_point, self.axis)
[docs] def rotation(self, center: design3d.Point3D, axis: design3d.Vector3D, angle: float): """ Revolution Surface 3D rotation. :param center: rotation center :param axis: rotation axis :param angle: angle rotation :return: a new rotated Revolution Surface 3D """ new_edge = self.edge.rotation(center, axis, angle) new_axis_point = self.axis_point.rotation(center, axis, angle) new_axis = self.axis.rotation(center, axis, angle) return RevolutionSurface3D(new_edge, new_axis_point, new_axis)
[docs] def simplify(self): """Gets the simplified version of the surface.""" line3d = curves.Line3D(self.axis_point, self.axis_point + self.axis) if isinstance(self.edge, edges.Arc3D): tore_center, _ = line3d.point_projection(self.edge.center) # Sphere if math.isclose(tore_center.point_distance(self.edge.center), 0., abs_tol=1e-6): return SphericalSurface3D(self.frame, self.edge.circle.radius, self.name) if isinstance(self.edge, (edges.LineSegment3D, curves.Line3D)): if isinstance(self.edge, edges.LineSegment3D): generatrix_line = self.edge.line else: generatrix_line = self.edge intersections = line3d.intersection(generatrix_line) if intersections: generatrix_line_direction = generatrix_line.unit_direction_vector() if self.axis.dot(generatrix_line_direction) > 0: semi_angle = design3d.geometry.vectors3d_angle(self.axis, generatrix_line_direction) else: semi_angle = design3d.geometry.vectors3d_angle(self.axis, -generatrix_line_direction) if not self.axis_point.is_close(intersections): new_w = self.axis_point - intersections new_w = new_w.unit_vector() new_frame = design3d.Frame3D(intersections, self.frame.u, new_w.cross(self.frame.u), new_w) else: new_frame = design3d.Frame3D(intersections, self.frame.u, self.frame.v, self.frame.w) return ConicalSurface3D(new_frame, semi_angle, name=self.name) generatrix_line_direction = generatrix_line.unit_direction_vector() if self.axis.is_colinear_to(generatrix_line_direction): radius = self.edge.point_distance(self.axis_point) return CylindricalSurface3D(self.frame, radius, self.name) return self
[docs] def u_closed_lower(self): """ Returns True if the surface is close in any of the u boundaries. """ a, b, c, _ = self.domain point_at_a_lower = self.point2d_to_3d(design3d.Point2D(a, c)) point_at_b_lower = self.point2d_to_3d(design3d.Point2D(0.5 * (a + b), c)) if point_at_b_lower.is_close(point_at_a_lower): return True return False
[docs] def u_closed_upper(self): """ Returns True if the surface is close in any of the u boundaries. """ a, b, _, d = self.domain point_at_a_upper = self.point2d_to_3d(design3d.Point2D(a, d)) point_at_b_upper = self.point2d_to_3d(design3d.Point2D(0.5 * (a + b), d)) if point_at_b_upper.is_close(point_at_a_upper): return True return False
[docs] def u_closed(self): """ Returns True if the surface is close in any of the u boundaries. """ return bool(self.u_closed_lower() or self.u_closed_upper())
[docs] def v_closed(self): """ Returns True if the surface is close in any of the u boundaries. """ return False
[docs] def is_singularity_point(self, point, *args, **kwargs): """Returns True if the point belongs to the surface singularity and False otherwise.""" tol = kwargs.get("tol", 1e-6) if self.u_closed_lower() and self.edge.start.is_close(point, tol): return True if self.u_closed_upper() and self.edge.end.is_close(point, tol): return True return False
[docs] def get_singularity_lines(self): """ Return lines that are parallel and coincident with surface singularity at parametric domain. """ a, b, c, d = self.domain lines = [] if self.u_closed_lower(): lines.append(curves.Line2D(design3d.Point2D(a, c), design3d.Point2D(b, c))) if self.u_closed_upper(): lines.append(curves.Line2D(design3d.Point2D(a, d), design3d.Point2D(b, d))) return lines
[docs] def u_iso(self, u: float) -> curves.Curve: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A curve :rtype: :class:`curves.Curve` """ if isinstance(self.edge, curves.Curve): return self.edge.rotation(self.axis_point, self.axis, u) return self.edge.curve().rotation(self.axis_point, self.axis, u)
[docs] def v_iso(self, v: float) -> curves.Circle3D: """ Returns the v-iso curve of the surface. :param v: The value of u where to extract the curve. :type v: float :return: A Circle 3D :rtype: :class:`curves.Circle3D` """ point_at_v = self.point2d_to_3d(design3d.Point2D(0.0, v)) axis_line = curves.Line3D.from_point_and_vector(self.axis_point, self.axis) frame_origin = axis_line.point_projection(point_at_v)[0] frame = self.frame.copy() frame.origin = frame_origin radius = axis_line.point_distance(point_at_v) return curves.Circle3D(frame, radius)
[docs] class BSplineSurface3D(Surface3D): """ A class representing a 3D B-spline surface. A B-spline surface is a smooth surface defined by a set of control points and a set of basis functions called B-spline basis functions. The shape of the surface is determined by the position of the control points and can be modified by moving the control points. :param degree_u: The degree of the B-spline curve in the u direction. :type degree_u: int :param degree_v: The degree of the B-spline curve in the v direction. :type degree_v: int :param control_points: A list of 3D control points that define the shape of the surface. :type control_points: List[`design3d.Point3D`] :param nb_u: The number of control points in the u direction. :type nb_u: int :param nb_v: The number of control points in the v direction. :type nb_v: int :param u_multiplicities: A list of multiplicities for the knots in the u direction. The multiplicity of a knot is the number of times it appears in the knot vector. :type u_multiplicities: List[int] :param v_multiplicities: A list of multiplicities for the knots in the v direction. The multiplicity of a knot is the number of times it appears in the knot vector. :type v_multiplicities: List[int] :param u_knots: A list of knots in the u direction. The knots are real numbers that define the position of the control points along the u direction. :type u_knots: List[float] :param v_knots: A list of knots in the v direction. The knots are real numbers that define the position of the control points along the v direction. :type v_knots: List[float] :param weights: (optional) A list of weights for the control points. The weights can be used to adjust the influence of each control point on the shape of the surface. Default is None. :type weights: List[float] :param name: (optional) A name for the surface. Default is an empty string. :type name: str """ face_class = "BSplineFace3D" _eq_is_data_eq = False def __init__(self, degree_u: int, degree_v: int, control_points: List[design3d.Point3D], nb_u: int, nb_v: int, u_multiplicities: List[int], v_multiplicities: List[int], u_knots: List[float], v_knots: List[float], weights: List[float] = None, name: str = ''): self.ctrlpts = np.asarray(control_points) self.degree_u = int(degree_u) self.degree_v = int(degree_v) self.nb_u = int(nb_u) self.nb_v = int(nb_v) self.u_knots = np.asarray(nurbs_helpers.standardize_knot_vector(u_knots), dtype=np.float64) self.v_knots = np.asarray(nurbs_helpers.standardize_knot_vector(v_knots), dtype=np.float64) self.u_multiplicities = np.asarray(u_multiplicities, dtype=np.int16) self.v_multiplicities = np.asarray(v_multiplicities, dtype=np.int16) self._weights = weights self.rational = False if weights is not None: self.rational = True self._weights = np.asarray(weights, dtype=np.float64) self._surface = None Surface3D.__init__(self, name=name) # Hidden Attributes self._displacements = None self._grids2d = None self._grids2d_deformed = None self._bbox = None self._surface_curves = None self._knotvector = None self.ctrlptsw = None if self._weights is not None: self.ctrlptsw = np.hstack((self.ctrlpts * self._weights[:, np.newaxis], self._weights[:, np.newaxis])) self._delta = [0.05, 0.05] self._eval_points = None self._vertices = None self._domain = None self._x_periodicity = False # Use False instead of None because None is a possible value of x_periodicity self._y_periodicity = False def __hash__(self): """ Creates custom hash to the surface. """ control_points = self.control_points if self.weights is None: return hash((tuple(control_points), self.degree_u, tuple(self.u_multiplicities), tuple(self.u_knots), self.nb_u, self.degree_v, tuple(self.v_multiplicities), tuple(self.v_knots), self.nb_v)) weights = tuple(self.weights) return hash((tuple(control_points), self.degree_u, tuple(self.u_multiplicities), tuple(self.u_knots), self.nb_u, self.degree_v, tuple(self.v_multiplicities), tuple(self.v_knots), self.nb_v, weights)) def __eq__(self, other): """ Defines the BSpline surface equality operation. """ if not isinstance(other, self.__class__): return False if (self.rational != other.rational or self.degree_u != other.degree_u or self.degree_v != other.degree_v or self.nb_u != other.nb_u or self.nb_v != other.nb_v): return False for s_k, o_k in zip(self.knotvector, other.knotvector): if len(s_k) != len(o_k) or any(not math.isclose(s, o, abs_tol=1e-8) for s, o in zip(s_k, o_k)): return False self_control_points = self.control_points other_control_points = other.control_points if len(self_control_points) != len(other_control_points) or \ any(not s_point.is_close(o_point) for s_point, o_point in zip(self_control_points, other_control_points)): return False if self.rational and other.rational: if len(self.weights) != len(other.weights) or \ any(not math.isclose(s_w, o_w, abs_tol=1e-8) for s_w, o_w in zip(self.weights, other.weights)): return False return True def _data_eq(self, other_object): """ Defines dessia common object equality. """ return self == other_object @property def data(self): """ Returns a dictionary of the BSpline data. """ datadict = { "degree": (self.degree_u, self.degree_v), "knotvector": self.knotvector, "size": (self.nb_u, self.nb_v), "sample_size": self.sample_size, "rational": self.rational, "precision": 18 } if self.rational: datadict["control_points"] = self.ctrlptsw else: datadict["control_points"] = self.ctrlpts return datadict @property def control_points(self): """Gets control points.""" return [design3d.Point3D(*point) for point in self.ctrlpts] @property def control_points_table(self): """Creates control points table.""" control_points_table = [] points_row = [] i = 1 for point in self.control_points: points_row.append(point) if i == self.nb_v: control_points_table.append(points_row) points_row = [] i = 1 else: i += 1 return control_points_table @property def knots_vector_u(self): """ Compute the global knot vector (u direction) based on knot elements and multiplicities. """ return np.repeat(self.u_knots, self.u_multiplicities) @property def knots_vector_v(self): """ Compute the global knot vector (v direction) based on knot elements and multiplicities. """ return np.repeat(self.v_knots, self.v_multiplicities) @property def knotvector(self): """ Knot vector in u and v direction respectively. """ if not self._knotvector: self._knotvector = [self.knots_vector_u, self.knots_vector_v] return self._knotvector @property def sample_size_u(self): """ Sample size for the u-direction. :getter: Gets sample size for the u-direction :setter: Sets sample size for the u-direction :type: int """ s_size = math.floor((1.0 / self.delta_u) + 0.5) return int(s_size) @sample_size_u.setter def sample_size_u(self, value): if not isinstance(value, int): raise ValueError("Sample size must be an integer value") knotvector_u = self.knots_vector_u # To make it operate like linspace, we have to know the starting and ending points. start_u = knotvector_u[self.degree_u] stop_u = knotvector_u[-(self.degree_u + 1)] # Set delta values self.delta_u = (stop_u - start_u) / float(value) @property def sample_size_v(self): """ Sample size for the v-direction. :getter: Gets sample size for the v-direction :setter: Sets sample size for the v-direction :type: int """ s_size = math.floor((1.0 / self.delta_v) + 0.5) return int(s_size) @sample_size_v.setter def sample_size_v(self, value): if not isinstance(value, int): raise ValueError("Sample size must be an integer value") knotvector_v = self.knots_vector_v # To make it operate like linspace, we have to know the starting and ending points. start_v = knotvector_v[self.degree_v] stop_v = knotvector_v[-(self.degree_v + 1)] # Set delta values self.delta_v = (stop_v - start_v) / float(value) @property def sample_size(self): """ Sample size for both u- and v-directions. :getter: Gets sample size as a tuple of values corresponding to u- and v-directions :setter: Sets sample size for both u- and v-directions :type: int """ sample_size_u = math.floor((1.0 / self.delta_u) + 0.5) sample_size_v = math.floor((1.0 / self.delta_v) + 0.5) return int(sample_size_u), int(sample_size_v) @sample_size.setter def sample_size(self, value): knotvector_u = self.knots_vector_u knotvector_v = self.knots_vector_v # To make it operate like linspace, we have to know the starting and ending points. start_u = knotvector_u[self.degree_u] stop_u = knotvector_u[-(self.degree_u + 1)] start_v = knotvector_v[self.degree_v] stop_v = knotvector_v[-(self.degree_v + 1)] # Set delta values self.delta_u = (stop_u - start_u) / float(value) self.delta_v = (stop_v - start_v) / float(value) @property def delta_u(self): """ Evaluation delta for the u-direction. :getter: Gets evaluation delta for the u-direction :setter: Sets evaluation delta for the u-direction :type: float """ return self._delta[0] @delta_u.setter def delta_u(self, value): # Delta value for surface evaluation should be between 0 and 1 if float(value) <= 0 or float(value) >= 1: raise ValueError("Surface evaluation delta (u-direction) must be between 0.0 and 1.0") # Set new delta value self._delta[0] = float(value) @property def delta_v(self): """ Evaluation delta for the v-direction. :getter: Gets evaluation delta for the v-direction :setter: Sets evaluation delta for the v-direction :type: float """ return self._delta[1] @delta_v.setter def delta_v(self, value): # Delta value for surface evaluation should be between 0 and 1 if float(value) <= 0 or float(value) >= 1: raise ValueError("Surface evaluation delta (v-direction) should be between 0.0 and 1.0") # Set new delta value self._delta[1] = float(value) @property def delta(self): """ Evaluation delta for both u- and v-directions. :getter: Gets evaluation delta as a tuple of values corresponding to u- and v-directions :setter: Sets evaluation delta for both u- and v-directions :type: float """ return self.delta_u, self.delta_v @delta.setter def delta(self, value): if isinstance(value, (int, float)): self.delta_u = value self.delta_v = value elif isinstance(value, (list, tuple)): if len(value) == 2: self.delta_u = value[0] self.delta_v = value[1] else: raise ValueError("Surface requires 2 delta values") else: raise ValueError("Cannot set delta. Please input a numeric value or a list or tuple with 2 numeric values") @property def weights(self): """ Gets the weights of the BSpline surface. """ if self._weights is None: return self._weights return self._weights.tolist() @property def x_periodicity(self): """ Evaluates the periodicity of the surface in u direction. """ if self._x_periodicity is False: a, b, c, d = self.domain point_at_a = self.point2d_to_3d(design3d.Point2D(a, 0.5 * (d - c))) point_at_b = self.point2d_to_3d(design3d.Point2D(b, 0.5 * (d - c))) if point_at_b.is_close(point_at_a) or self.u_closed: self._x_periodicity = b - a else: self._x_periodicity = None return self._x_periodicity @property def y_periodicity(self): """ Evaluates the periodicity of the surface in v direction. """ if self._y_periodicity is False: a, b, c, d = self.domain point_at_c = self.point2d_to_3d(design3d.Point2D(0.5 * (b - a), c)) point_at_d = self.point2d_to_3d(design3d.Point2D(0.5 * (b - a), d)) if point_at_d.is_close(point_at_c) or self.v_closed: self._y_periodicity = d - c else: self._y_periodicity = None return self._y_periodicity @property def bounding_box(self): """Gets the Bounding box of the BSpline Surface 3d.""" if not self._bbox: self._bbox = self._bounding_box() return self._bbox def _bounding_box(self): """ Computes the bounding box of the surface. """ points = self.evalpts xmin = np.min(points[:, 0]) ymin = np.min(points[:, 1]) zmin = np.min(points[:, 2]) xmax = np.max(points[:, 0]) ymax = np.max(points[:, 1]) zmax = np.max(points[:, 2]) return design3d.core.BoundingBox(xmin, xmax, ymin, ymax, zmin, zmax) @property def surface_curves(self): """ Extracts curves from a surface. """ if not self._surface_curves: self._surface_curves = self.get_surface_curves() return self._surface_curves
[docs] def get_surface_curves(self, **kwargs): """ Extracts curves from a surface. """ # Get keyword arguments extract_u = kwargs.get('extract_u', True) extract_v = kwargs.get('extract_v', True) cpts = self.control_points # v-direction crvlist_v = [] weights = [] if extract_v: for u in range(self.nb_u): control_points = [cpts[v + (self.nb_v * u)] for v in range(self.nb_v)] if self.rational: weights = [self.weights[v + (self.nb_v * u)] for v in range(self.nb_v)] curve = edges.BSplineCurve3D(self.degree_v, control_points, self.v_multiplicities, self.v_knots, weights) crvlist_v.append(curve) # u-direction crvlist_u = [] if extract_u: for v in range(self.nb_v): control_points = [cpts[v + (self.nb_v * u)] for u in range(self.nb_u)] if self.rational: weights = [self.weights[v + (self.nb_v * u)] for u in range(self.nb_u)] curve = edges.BSplineCurve3D(self.degree_u, control_points, self.u_multiplicities, self.u_knots, weights) crvlist_u.append(curve) # Return shapes as a dict object return {"u": crvlist_u, "v": crvlist_v}
[docs] def extract_curves(self, u: List[float] = None, v: List[float] = None): """ Extracts curves from a surface. :param u: a list of parameters in ascending order in u direction to extract curves :param v: a list of parameters in ascending order in v direction to extract curves :return: a dictionary containing the extracted curves in u and v direction :rtype: dict """ umin, umax, d3din, d3dax = self.domain def extract_from_surface_boundary_u(u_pos): weights = None control_points = [self.control_points[j + (self.nb_v * u_pos)] for j in range(self.nb_v)] if self.rational: weights = [self.weights[j + (self.nb_v * u_pos)] for j in range(self.nb_v)] return edges.BSplineCurve3D(self.degree_v, control_points, self.v_multiplicities, self.v_knots, weights) def extract_from_surface_boundary_v(v_pos): weights = None control_points = [self.control_points[v_pos + (self.nb_v * i)] for i in range(self.nb_u)] if self.rational: weights = [self.weights[v_pos + (self.nb_v * i)] for i in range(self.nb_u)] return edges.BSplineCurve3D(self.degree_u, control_points, self.u_multiplicities, self.u_knots, weights) # v-direction crvlist_v = [] if v: for param in v: if abs(param - d3din) < 1e-6: crvlist_v.append(extract_from_surface_boundary_v(0)) elif abs(param - d3dax) < 1e-6: crvlist_v.append(extract_from_surface_boundary_v(self.nb_v - 1)) else: curve = extract_surface_curve_v(self, param, edges.BSplineCurve3D) crvlist_v.append(curve) # u-direction crvlist_u = [] if u: for param in u: if abs(param - umin) < 1e-6: crvlist_u.append(extract_from_surface_boundary_u(0)) elif abs(param - umax) < 1e-6: crvlist_u.append(extract_from_surface_boundary_u(self.nb_u - 1)) else: curve = extract_surface_curve_u(self, param, edges.BSplineCurve3D) crvlist_u.append(curve) # Return shapes as a dict object return {"u": crvlist_u, "v": crvlist_v}
[docs] def u_iso(self, u: float) -> edges.BSplineCurve3D: """ Returns the u-iso curve of the surface. :param u: The value of u where to extract the curve. :type u: float :return: A line 3D :rtype: :class:`curves.Line3D` """ return self.extract_curves(u=[u])["u"][0]
[docs] def v_iso(self, v: float) -> edges.BSplineCurve3D: """ Returns the v-iso curve of the surface. :param v: The value of v where to extract the curve. :type u: float :return: A BSpline curve 3D :rtype: :class:`edges.BSplineCurve3D` """ return self.extract_curves(v=[v])["v"][0]
[docs] def evaluate(self, **kwargs): """ Evaluates the surface. The evaluated points are stored in :py:attr:`evalpts` property. Keyword Arguments: * ``start_u``: start parameter on the u-direction * ``stop_u``: stop parameter on the u-direction * ``start_v``: start parameter on the v-direction * ``stop_v``: stop parameter on the v-direction The ``start_u``, ``start_v`` and ``stop_u`` and ``stop_v`` parameters allow evaluation of a surface segment in the range *[start_u, stop_u][start_v, stop_v]* i.e. the surface will also be evaluated at the ``stop_u`` and ``stop_v`` parameter values. """ knotvector_u = self.knots_vector_u knotvector_v = self.knots_vector_v # Find evaluation start and stop parameter values start_u = kwargs.get('start_u', knotvector_u[self.degree_u]) stop_u = kwargs.get('stop_u', knotvector_u[-(self.degree_u + 1)]) start_v = kwargs.get('start_v', knotvector_v[self.degree_v]) stop_v = kwargs.get('stop_v', knotvector_v[-(self.degree_v + 1)]) # Evaluate and cache self._eval_points = np.asarray(evaluate_surface(self.data, start=(start_u, start_v), stop=(stop_u, stop_v)), dtype=np.float64)
@property def evalpts(self): """ Evaluated points. :getter: Gets the coordinates of the evaluated points :type: list """ if self._eval_points is None or len(self._eval_points) == 0: self.evaluate() return self._eval_points @property def u_domain(self): """The parametric domain of the surface in the U direction.""" knotvector_u = self.knots_vector_u start_u = knotvector_u[self.degree_u] stop_u = knotvector_u[-(self.degree_u + 1)] return start_u, stop_u @property def v_domain(self): """The parametric domain of the surface in the V direction.""" knotvector_v = self.knots_vector_v # Find evaluation start and stop parameter values start_v = knotvector_v[self.degree_v] stop_v = knotvector_v[-(self.degree_v + 1)] return start_v, stop_v @property def domain(self): """ Domain. Domain is determined using the knot vector(s). :getter: Gets the domain """ if not self._domain: umin, umax = self.u_domain d3din, d3dax = self.v_domain self._domain = umin, umax, d3din, d3dax return self._domain
[docs] def copy(self, deep: bool = True, **kwargs): """ Returns a copy of the instance. :param deep: If False, perform a shallow copy. If True, perform a deep copy. """ if deep: weights = None if self.rational: weights = self._weights.copy() return self.__class__(self.degree_u, self.degree_v, self.control_points, self.nb_u, self.nb_v, self.u_multiplicities.copy(), self.v_multiplicities.copy(), self.u_knots.copy(), self.v_knots.copy(), weights, name=self.name + "_copy") return self.__class__(self.degree_u, self.degree_v, self.control_points, self.nb_u, self.nb_v, self.u_multiplicities, self.v_multiplicities, self.u_knots, self.v_knots, self.weights, name=self.name + "_copy")
[docs] def to_geomdl(self): """Translate into a geomdl object.""" if not self._surface: if self._weights is None: surface = BSpline.Surface() points = self.ctrlpts.tolist() else: surface = NURBS.Surface() points = [(control_point[0] * self._weights[i], control_point[1] * self._weights[i], control_point[2] * self._weights[i], self._weights[i]) for i, control_point in enumerate(self.control_points)] surface.degree_u = self.degree_u surface.degree_v = self.degree_v surface.set_ctrlpts(points, self.nb_u, self.nb_v) knot_vector = self.knotvector surface.knotvector_u = knot_vector[0] surface.knotvector_v = knot_vector[1] surface.delta = 0.05 self._surface = surface return self._surface
[docs] def to_dict(self, *args, **kwargs): """Avoids storing points in memo that makes serialization slow.""" dict_ = self.base_dict() dict_['degree_u'] = self.degree_u dict_['degree_v'] = self.degree_v dict_['control_points'] = [point.to_dict() for point in self.control_points] dict_['nb_u'] = self.nb_u dict_['nb_v'] = self.nb_v dict_['u_multiplicities'] = self.u_multiplicities.tolist() dict_['v_multiplicities'] = self.v_multiplicities.tolist() dict_['u_knots'] = self.u_knots.tolist() dict_['v_knots'] = self.v_knots.tolist() dict_['weights'] = self.weights return dict_
[docs] def ctrlpts2d(self): """ Each row represents the control points in u direction and each column the points in v direction. """ ctrlpts = self.ctrlptsw if self.rational else self.ctrlpts return np.reshape(ctrlpts, (self.nb_u, self.nb_v, -1))
[docs] def vertices(self): """ Evaluated points. :getter: Gets the coordinates of the evaluated points :type: list """ u_min, u_max, v_min, v_max = self.domain if self._vertices is None or len(self._vertices) == 0: vertices = [] u_vector = np.linspace(u_min, u_max, self.sample_size_u, dtype=np.float64) v_vector = np.linspace(v_min, v_max, self.sample_size_v, dtype=np.float64) for u in u_vector: for v in v_vector: vertices.append((u, v)) self._vertices = vertices return self._vertices
[docs] def points(self): """ Returns surface points. """ return [design3d.Point3D(*point) for point in self.evalpts]
[docs] def control_points_matrix(self, coordinates): """ Define control points like a matrix, for each coordinate: x:0, y:1, z:2. """ points = np.empty((self.nb_u, self.nb_v)) for i in range(0, self.nb_u): for j in range(0, self.nb_v): points[i][j] = self.control_points_table[i][j][coordinates] return points
[docs] def basis_functions_u(self, u, k, i): """ Compute basis functions Bi in u direction for u=u and degree=k. """ # k = self.degree_u knots_vector_u = self.knots_vector_u if k == 0: return 1.0 if knots_vector_u[i] <= u < knots_vector_u[i + 1] else 0.0 if knots_vector_u[i + k] == knots_vector_u[i]: param_c1 = 0.0 else: param_c1 = (u - knots_vector_u[i]) / (knots_vector_u[i + k] - knots_vector_u[i]) \ * self.basis_functions_u(u, k - 1, i) if knots_vector_u[i + k + 1] == knots_vector_u[i + 1]: param_c2 = 0.0 else: param_c2 = (knots_vector_u[i + k + 1] - u) / (knots_vector_u[i + k + 1] - knots_vector_u[i + 1]) * \ self.basis_functions_u(u, k - 1, i + 1) return param_c1 + param_c2
[docs] def basis_functions_v(self, v, k, i): """ Compute basis functions Bi in v direction for v=v and degree=k. """ # k = self.degree_u knots = self.knots_vector_v if k == 0: return 1.0 if knots[i] <= v < knots[i + 1] else 0.0 if knots[i + k] == knots[i]: param_c1 = 0.0 else: param_c1 = (v - knots[i]) / (knots[i + k] - knots[i]) * self.basis_functions_v(v, k - 1, i) if knots[i + k + 1] == knots[i + 1]: param_c2 = 0.0 else: param_c2 = (knots[i + k + 1] - v) / (knots[i + k + 1] - knots[i + 1]) * self.basis_functions_v(v, k - 1, i + 1) return param_c1 + param_c2
[docs] def derivatives(self, u, v, order): """ Evaluates n-th order surface derivatives at the given (u, v) parameter pair. :param u: Point's u coordinate. :type u: float :param v: Point's v coordinate. :type v: float :param order: Order of the derivatives. :type order: int :return: A list SKL, where SKL[k][l] is the derivative of the surface S(u,v) with respect to u k times and v l times :rtype: List[`design3d.Vector3D`] """ if self.weights is not None: control_points = self.ctrlptsw else: control_points = self.ctrlpts derivatives = derivatives_surface([self.degree_u, self.degree_v], self.knotvector, control_points, [self.nb_u, self.nb_v], self.rational, [u, v], order) for i in range(order + 1): for j in range(order + 1): derivatives[i][j] = design3d.Vector3D(*derivatives[i][j]) return derivatives
[docs] def blending_vector_u(self, u): """ Compute a vector of basis_functions in u direction for u=u. """ blending_vect = np.empty((1, self.nb_u)) for j in range(0, self.nb_u): blending_vect[0][j] = self.basis_functions_u(u, self.degree_u, j) return blending_vect
[docs] def blending_vector_v(self, v): """ Compute a vector of basis_functions in v direction for v=v. """ blending_vect = np.empty((1, self.nb_v)) for j in range(0, self.nb_v): blending_vect[0][j] = self.basis_functions_v(v, self.degree_v, j) return blending_vect
[docs] def blending_matrix_u(self, u): """ Compute a matrix of basis_functions in u direction for a vector u like [0,1]. """ blending_mat = np.empty((len(u), self.nb_u)) for i, u_i in enumerate(u): for j in range(self.nb_u): blending_mat[i][j] = self.basis_functions_u(u_i, self.degree_u, j) return blending_mat
[docs] def blending_matrix_v(self, v): """ Compute a matrix of basis_functions in v direction for a vector v like [0,1]. """ blending_mat = np.empty((len(v), self.nb_v)) for i, v_i in enumerate(v): for j in range(self.nb_v): blending_mat[i][j] = self.basis_functions_v(v_i, self.degree_v, j) return blending_mat
[docs] @lru_cache(maxsize=6) def decompose(self, return_params: bool = False, decompose_dir="uv"): """ Decomposes the surface into Bezier surface patches of the same degree. :param return_params: If True, returns the parameters from start and end of each Bézier patch with repect to the input curve. :type return_params: bool :param decompose_dir: Direction of decomposition. 'uv', 'u' or 'v'. :type decompose_dir: str """ return decompose_surface(self, return_params, decompose_dir=decompose_dir)
[docs] def point2d_to_3d(self, point2d: design3d.Point2D): """ Evaluate the surface at a given parameter coordinate. """ u, v = point2d umin, umax, d3din, d3dax = self.domain u = float(min(max(u, umin), umax)) v = float(min(max(v, d3din), d3dax)) point_array = evaluate_surface(self.data, start=(u, v), stop=(u, v))[0] return design3d.Point3D(*point_array)
def _get_grid_bounds(self, params, delta_u, delta_v): """ Update bounds and grid_size at each iteration of point inversion grid search. """ u, v = params if u == self.domain[0]: u_start = self.domain[0] u_stop = self.domain[0] sample_size_u = 1 elif u == self.domain[1]: u_start = self.domain[1] u_stop = self.domain[1] sample_size_u = 1 else: u_start = max(u - delta_u, self.domain[0]) u_stop = min(u + delta_u, self.domain[1]) sample_size_u = 10 if v == self.domain[2]: v_start = self.domain[2] v_stop = self.domain[2] sample_size_v = 1 elif v == self.domain[3]: v_start = self.domain[3] v_stop = self.domain[3] sample_size_v = 1 else: v_start = max(v - delta_v, self.domain[2]) v_stop = min(v + delta_v, self.domain[3]) sample_size_v = 10 return u_start, u_stop, v_start, v_stop, sample_size_u, sample_size_v def _update_parameters(self, bounds, sample_size_u, sample_size_v, index): """ Helper function to update parameters of point inversion grid search at each iteration. """ u_start, u_stop, v_start, v_stop = bounds if sample_size_u == 1: delta_u = 0.0 u = u_start delta_v = (v_stop - v_start) / (sample_size_v - 1) v = v_start + index * delta_v elif sample_size_v == 1: delta_u = (u_stop - u_start) / (sample_size_u - 1) u = u_start + index * delta_u delta_v = 0.0 v = v_start else: u, v, delta_u, delta_v = self._get_params_from_evaluation_position_bounds_and_sizes(index, bounds, sample_size_u, sample_size_v) return u, v, delta_u, delta_v @staticmethod def _find_index_min(matrix_points, point): """Helper function to find point of minimal distance.""" distances = np.linalg.norm(matrix_points - point, axis=1) indexes = np.argsort(distances) index = indexes[0] return index, distances[index] def _point_inversion_initialization(self, point3d_array): """ Helper function to initialize parameters. """ if self.nb_u > 15 * self.nb_v: self.sample_size_u, self.sample_size_v = 80, 5 elif self.nb_v > 15 * self.nb_u: self.sample_size_u, self.sample_size_v = 5, 80 initial_index, minimal_distance = self._find_index_min(self.evalpts, point3d_array) u, v, delta_u, delta_v = self._get_params_from_evaluation_position_bounds_and_sizes(initial_index, self.domain, self.sample_size_u, self.sample_size_v) u_start, u_stop, v_start, v_stop = self.domain sample_size_u = 10 sample_size_v = 10 if u == u_start: u_stop = u + delta_u sample_size_u = 5 elif u == u_stop: u_start = u - delta_u sample_size_u = 5 else: u_start = max(u - delta_u, self.domain[0]) u_stop = min(u + delta_u, self.domain[1]) if v == v_start: v_stop = v + delta_v sample_size_v = 5 elif v == v_stop: v_start = v - delta_v sample_size_v = 5 else: v_start = max(v - delta_v, self.domain[2]) v_stop = min(v + delta_v, self.domain[3]) return u, v, u_start, u_stop, v_start, v_stop, delta_u, delta_v, sample_size_u, sample_size_v, minimal_distance def _helper_point_inversion_grid_search_update_evaluation_data(self, sample_size_u, sample_size_v): """ Helper function to get the evaluation data of the surface adding a given sample size in both u and v direction. This function is required for performance and coherence purposes to avoid modifying the surface sample size in each iteration. """ datadict = { "degree": (self.degree_u, self.degree_v), "knotvector": self.knotvector, "size": (self.nb_u, self.nb_v), "sample_size": [sample_size_u, sample_size_v], "rational": self.rational, "precision": 18 } if self.rational: datadict["control_points"] = self.ctrlptsw else: datadict["control_points"] = self.ctrlpts return datadict @staticmethod def _get_params_from_evaluation_position_bounds_and_sizes(index, bounds, sample_size_u, sample_size_v): """ Gets the values of u, v of an evalution point from its index in a list that follows a known structure. """ u_start, u_stop, v_start, v_stop = bounds u_idx = int(index / sample_size_v) v_idx = index % sample_size_v delta_u = (u_stop - u_start) / (sample_size_u - 1) delta_v = (v_stop - v_start) / (sample_size_v - 1) u = u_start + u_idx * delta_u v = v_start + v_idx * delta_v return u, v, delta_u, delta_v
[docs] def point3d_to_2d(self, point3d: design3d.Point3D, tol=1e-6): """ Evaluates the parametric coordinates (u, v) of a 3D point (x, y, z). :param point3d: A 3D point to be evaluated. :type point3d: :class:`design3d.Point3D` :param tol: Tolerance to accept the results. :type tol: float :return: The parametric coordinates (u, v) of the point. :rtype: :class:`design3d.Point2D` """ umin, umax, d3din, d3dax = self.domain point = None if self.is_singularity_point(point3d, tol=tol): if self.u_closed_upper(tol) and point3d.is_close(self.point2d_to_3d(design3d.Point2D(umin, d3dax)), tol): point = design3d.Point2D(umin, d3dax) elif self.u_closed_lower(tol) and point3d.is_close(self.point2d_to_3d(design3d.Point2D(umin, d3din)), tol): point = design3d.Point2D(umin, d3din) elif self.v_closed_upper(tol) and point3d.is_close(self.point2d_to_3d(design3d.Point2D(umax, d3din)), tol): return design3d.Point2D(umax, d3din) elif self.v_closed_lower(tol) and point3d.is_close(self.point2d_to_3d(design3d.Point2D(umin, d3din)), tol): point = design3d.Point2D(umin, d3din) if point: return point x0, distance = self.point_inversion_grid_search(point3d, 5e-5) if distance < tol: return design3d.Point2D(*x0) x1, _, distance = self.point_inversion(x0, point3d, tol) if distance <= tol: return design3d.Point2D(*x1) return self.point3d_to_2d_minimize(point3d, x0, distance, tol)
[docs] def point3d_to_2d_minimize(self, point3d, initial_guess, point_inversion_result, tol): """Auxiliary function for point3d_to_2d in case the point inversion does not converge.""" def fun(x): derivatives = self.derivatives(x[0], x[1], 1) vector = derivatives[0][0] - point3d f_value = vector.norm() if f_value == 0.0: jacobian = np.array([0.0, 0.0]) else: jacobian = np.array([vector.dot(derivatives[1][0]) / f_value, vector.dot(derivatives[0][1]) / f_value]) return f_value, jacobian u_start, u_stop, v_start, v_stop = self.domain results = [] res = minimize(fun, x0=np.array(initial_guess), jac=True, bounds=[(u_start, u_stop), (v_start, v_stop)]) if res.fun <= tol or (tol > 1e-7 and res.success and abs(res.fun - point_inversion_result) <= tol and res.fun < 5 * tol): return design3d.Point2D(*res.x) results = [(res.x, res.fun)] if self.u_closed: res = minimize(fun, x0=np.array((u_start, initial_guess[1])), jac=True, bounds=[(u_start, u_stop), (v_start, v_stop)]) if res.fun <= tol: return design3d.Point2D(u_start, initial_guess[1]) results.append((res.x, res.fun)) res = minimize(fun, x0=np.array((u_stop, initial_guess[1])), jac=True, bounds=[(u_start, u_stop), (v_start, v_stop)]) if res.fun <= tol: return design3d.Point2D(u_stop, initial_guess[1]) results.append((res.x, res.fun)) if self.v_closed: res = minimize(fun, x0=np.array((initial_guess[0], v_start)), jac=True, bounds=[(u_start, u_stop), (v_start, v_stop)]) results.append((res.x, res.fun)) if res.fun <= tol: return design3d.Point2D(initial_guess[0], v_start) res = minimize(fun, x0=np.array((initial_guess[0], v_stop)), jac=True, bounds=[(u_start, u_stop), (v_start, v_stop)]) if res.fun <= tol: return design3d.Point2D(initial_guess[0], v_stop) results.append((res.x, res.fun)) point3d_array = np.asarray(point3d) if self.u_knots.shape[0] > 2 or self.v_knots.shape[0] > 2: decompose_dir = "uv" if self.u_closed: decompose_dir = "v" if self.v_closed: decompose_dir = "u" for patch, param in self.decompose(return_params=True, decompose_dir=decompose_dir): xmin, ymin, zmin = patch.ctrlpts.min(axis=0) xmax, ymax, zmax = patch.ctrlpts.max(axis=0) bbox = design3d.core.BoundingBox(xmin, xmax, ymin, ymax, zmin, zmax) if bbox.point_inside(point3d): distances = np.linalg.norm(patch.evalpts - point3d_array, axis=1) index = np.argmin(distances) u_start, u_stop, v_start, v_stop = patch.domain delta_u = (u_stop - u_start) / (patch.sample_size_u - 1) delta_v = (v_stop - v_start) / (patch.sample_size_v - 1) u_idx = int(index / patch.sample_size_v) v_idx = index % patch.sample_size_v u = u_start + u_idx * delta_u v = v_start + v_idx * delta_v x1, _, distance = patch.point_inversion((u, v), point3d, 1e-6) u = x1[0] * (param[0][1] - param[0][0]) + param[0][0] v = x1[1] * (param[1][1] - param[1][0]) + param[1][0] if distance < 5e-6: return design3d.Point2D(u, v) results.append(((u, v), distance)) distances = np.linalg.norm(self.evalpts - point3d_array, axis=1) indexes = np.argsort(distances) delta_u = (u_stop - u_start) / (self.sample_size_u - 1) delta_v = (v_stop - v_start) / (self.sample_size_v - 1) if self.weights is not None: control_points = self.ctrlptsw else: control_points = self.ctrlpts for index in indexes[:2]: if index == 0: u_idx, v_idx = 0, 0 else: u_idx = int(index / self.sample_size_v) v_idx = index % self.sample_size_v u = u_start + u_idx * delta_u v = v_start + v_idx * delta_v x0 = (u, v) res = point_inversion(point3d_array, x0, [(u_start, u_stop), (v_start, v_stop)], [self.degree_u, self.degree_v], self.knotvector, control_points, [self.nb_u, self.nb_v], self.rational) if res.fun < 1e-6: return design3d.Point2D(*res.x) results.append((res.x, res.fun)) return design3d.Point2D(*min(results, key=lambda r: r[1])[0])
[docs] def point_inversion(self, x, point3d, tol, maxiter: int = 50): """ Performs point inversion. Given a point P = (x, y, z) assumed to lie on the NURBS surface S(u, v), point inversion is the problem of finding the corresponding parameters u, v that S(u, v) = P. """ jacobian, k, surface_derivatives, distance_vector = self.point_inversion_funcs(x, point3d) dist, check = self.check_convergence(surface_derivatives, distance_vector, tol1=tol) if maxiter == 1: return x, False, dist if check: return x, True, dist if maxiter == 1: return x, False, dist if jacobian[1][1]: lu, piv = lu_factor(jacobian) delta = lu_solve((lu, piv), k) new_x = [delta[0][0] + x[0], delta[1][0] + x[1]] new_x = self.check_bounds(new_x) else: new_x = x residual = (new_x[0] - x[0]) * surface_derivatives[1][0] + (new_x[1] - x[1]) * surface_derivatives[0][1] if residual.norm() <= 1e-12: return x, False, dist x = new_x return self.point_inversion(x, point3d, tol, maxiter=maxiter - 1)
[docs] def point_inversion_funcs(self, x, point3d): """Returns functions evaluated at x.""" surface_derivatives = self.derivatives(x[0], x[1], 2) distance_vector = surface_derivatives[0][0] - point3d common_term = (surface_derivatives[1][0].dot(surface_derivatives[0][1]) + distance_vector.dot(surface_derivatives[1][1])) jacobian = np.asarray( [[surface_derivatives[1][0].norm() ** 2 + distance_vector.dot(surface_derivatives[2][0]), common_term], [common_term, surface_derivatives[0][1].norm() ** 2 + distance_vector.dot(surface_derivatives[0][2])]]) k = np.asarray( [[-(distance_vector.dot(surface_derivatives[1][0]))], [-(distance_vector.dot(surface_derivatives[0][1]))]]) return jacobian, k, surface_derivatives, distance_vector
[docs] @staticmethod def check_convergence(surf_derivatives, distance_vector, tol1: float = 1e-6, tol2: float = 1e-8): """Check convergence of point inversion method.""" dist = distance_vector.norm() if dist <= tol1: return dist, True zero_cos_u = abs(surf_derivatives[1][0].dot(distance_vector)) / ( (surf_derivatives[1][0].norm() + 1e-12) * dist) zero_cos_v = abs(surf_derivatives[0][1].dot(distance_vector)) / ( (surf_derivatives[0][1].norm() + 1e-12) * dist) if zero_cos_u <= tol2 and zero_cos_v <= tol2: return dist, True return dist, False
[docs] def check_bounds(self, x): """Check surface bounds.""" u, v = x a, b, c, d = self.domain if u < a: u = a elif u > b: u = b if v < c: v = c elif v > d: v = d x[0] = u x[1] = v return x
[docs] def parametric_points_to_3d(self, points: NDArray[np.float64]) -> NDArray[np.float64]: """ Transform parametric coordinates to 3D points on the BSpline surface. Given a set of parametric coordinates `(u, v)` representing points on the surface, this method returns the corresponding 3D points on the BSpline surface. :param points: Parametric coordinates in the form of a numpy array with shape (n, 2), where `n` is the number of points, and each row corresponds to `(u, v)`. :type points: numpy.ndarray[np.float64] :return: Array of 3D points representing the BSpline surface in Cartesian coordinates. :rtype: numpy.ndarray[np.float64] """ umin, umax, d3din, d3dax = self.domain params = [(float(min(max(u, umin), umax)), float(min(max(v, d3din), d3dax))) for u, v in points] return np.asarray([evaluate_surface(self.data, start=param, stop=param)[0] for param in params], dtype=np.float64)
[docs] def linesegment2d_to_3d(self, linesegment2d): """Evaluates the Euclidean form for the parametric line segment.""" points = [] direction_vector = linesegment2d.unit_direction_vector(0.0) start3d = self.point2d_to_3d(linesegment2d.start) end3d = self.point2d_to_3d(linesegment2d.end) if direction_vector.is_colinear_to(design3d.X2D): curve = self.v_iso(linesegment2d.start.y) if linesegment2d.start.x > linesegment2d.end.x: curve = curve.reverse() return [curve.trim(start3d, end3d)] if direction_vector.is_colinear_to(design3d.Y2D): curve = self.u_iso(linesegment2d.start.x) if linesegment2d.start.y > linesegment2d.end.y: curve = curve.reverse() return [curve.trim(start3d, end3d)] n = 20 for point in linesegment2d.discretization_points(number_points=n): point3d = self.point2d_to_3d(point) if not point3d.in_list(points): points.append(point3d) if len(points) < 2: return None if len(points) == 2: return [design3d.edges.LineSegment3D(points[0], points[-1])] if len(points) < min(self.degree_u, self.degree_v) + 1: bspline = edges.BSplineCurve3D.from_points_interpolation(points, 2, centripetal=True) return [bspline] bspline = edges.BSplineCurve3D.from_points_interpolation(points, min(self.degree_u, self.degree_v), centripetal=True) return [bspline.simplify]
[docs] def linesegment3d_to_2d(self, linesegment3d): """ A line segment on a BSplineSurface3D will be in any case a line in 2D?. """ tol = 1e-6 if linesegment3d.length() > 1e-5 else 1e-7 if self.u_closed or self.v_closed: discretization_points = linesegment3d.discretization_points(number_points=3) parametric_points = [self.point3d_to_2d(point, tol) for point in discretization_points] start, _, end = self._fix_start_end_singularity_point_at_parametric_domain(linesegment3d, parametric_points, discretization_points, tol) else: start = self.point3d_to_2d(linesegment3d.start, tol) end = self.point3d_to_2d(linesegment3d.end, tol) umin, umax, d3din, d3dax = self.domain if self.x_periodicity and \ (math.isclose(end.x, umin, abs_tol=1e-3) or math.isclose(end.x, umax, abs_tol=1e-3)): end.x = start.x if self.y_periodicity and \ (math.isclose(end.y, d3din, abs_tol=1e-3) or math.isclose(end.y, d3dax, abs_tol=1e-3)): end.y = start.y if start.is_close(end): return None return [edges.LineSegment2D(start, end)]
def _repair_periodic_boundary_points(self, edge3d, points, direction_periodicity): """ Verifies points at boundary on a periodic BSplineSurface3D. :param points: List of `design3d.Point2D` after transformation from 3D Cartesian coordinates :type points: List[design3d.Point2D] :param direction_periodicity: should be 'x' if x_periodicity or 'y' if y periodicity :type direction_periodicity: str """ lth = edge3d.length() pt_after_start = self.point3d_to_2d(edge3d.point_at_abscissa(0.15 * lth)) pt_before_end = self.point3d_to_2d(edge3d.point_at_abscissa(0.85 * lth)) min_bound_x, max_bound_x, min_bound_y, max_bound_y = self.domain if direction_periodicity == 'x': i = 0 min_bound, max_bound = min_bound_x, max_bound_x periodicity = self.x_periodicity else: i = 1 min_bound, max_bound = min_bound_y, max_bound_y periodicity = self.y_periodicity start = points[0] end = points[-1] delta = max_bound + min_bound if math.isclose(start[i], min_bound, abs_tol=1e-4) and pt_after_start[i] > 0.5 * delta: start[i] = max_bound elif math.isclose(start[i], max_bound, abs_tol=1e-4) and pt_after_start[i] < 0.5 * delta: start[i] = min_bound if math.isclose(end[i], min_bound, abs_tol=1e-4) and pt_before_end[i] > 0.5 * delta: end[i] = max_bound elif math.isclose(end[i], max_bound, abs_tol=1e-4) and pt_before_end[i] < 0.5 * delta: end[i] = min_bound points[0] = start points[-1] = end delta_i = abs(points[-1][i] - points[0][i]) if ((delta_i <= 1e-5 or math.isclose(delta_i, periodicity, abs_tol=1e-3)) and all((math.isclose(p[i], max_bound, abs_tol=1e-2) or math.isclose(p[i], min_bound, abs_tol=1e-2)) for p in points)): # if the line is at the boundary of the surface domain, we take the first point as reference t_param = max_bound if math.isclose(points[0][i], max_bound, abs_tol=1e-4) else min_bound if direction_periodicity == 'x': points = [design3d.Point2D(t_param, p[1]) for p in points] else: points = [design3d.Point2D(p[0], t_param) for p in points] return points def _repair_points_order(self, points, edge3d, surface_domain, direction_periodicity): """Helper function to reorder edge discretization points on parametric domain.""" min_bound_x, max_bound_x, min_bound_y, max_bound_y = surface_domain line_at_periodicity = edges.LineSegment3D( self.point2d_to_3d(design3d.Point2D(min_bound_x, min_bound_y)), self.point2d_to_3d(design3d.Point2D( min_bound_x if direction_periodicity == 'x' else max_bound_x, min_bound_y if direction_periodicity == 'y' else max_bound_y )) ) if line_at_periodicity.point_belongs(edge3d.start) or line_at_periodicity.point_belongs(edge3d.end): return points intersections = edge3d.intersections(line_at_periodicity) if not intersections or len(intersections) > 1: return points point_at_periodicity = self.point3d_to_2d(intersections[0]) index_periodicity = design3d.core.get_point_index_in_list(point_at_periodicity, points) if index_periodicity is not None: if edge3d.periodic: points = [point_at_periodicity] + points[index_periodicity + 1:-1] + points[:index_periodicity + 1] else: points = [point_at_periodicity] + points[index_periodicity + 1:] + points[:index_periodicity + 1] else: sign = points[1].x - points[0].x if direction_periodicity == 'x' else points[1].y - points[0].y for i, (point, next_point) in enumerate(zip(points[:-1], points[1:])): if sign * (next_point.x - point.x if direction_periodicity == 'x' else next_point.y - point.y) < 0: index_periodicity = i break if edge3d.periodic: points = ([point_at_periodicity] + points[index_periodicity + 1: -1] + points[:index_periodicity + 1] + [point_at_periodicity]) else: points = ([point_at_periodicity] + points[index_periodicity + 1:] + points[:index_periodicity + 1] + [point_at_periodicity]) return points def _fix_start_end_singularity_point_at_parametric_domain(self, edge3d, parametric_points, discretization_points, tol): """ Helper function to fix start and end points on surfaces with singularities. """ start = parametric_points[0].copy() end = parametric_points[-1].copy() point_after_start = self.point3d_to_2d(edge3d.point_at_abscissa(0.02 * edge3d.length())) point_before_end = self.point3d_to_2d(edge3d.point_at_abscissa(0.98 * edge3d.length())) fixed = False if start_flag := self.is_singularity_point(edge3d.start) and self._is_line_segment(parametric_points[1:]): direction_vector = parametric_points[-1] - parametric_points[-2] if direction_vector.is_colinear_to(design3d.X2D, 1e-2): start.y = point_after_start.y parametric_points[0] = start fixed = True elif direction_vector.is_colinear_to(design3d.Y2D, 1e-2): start.x = point_after_start.x parametric_points[0] = start fixed = True else: fixed = False if end_flag := self.is_singularity_point(edge3d.end) and self._is_line_segment(parametric_points[:-1]): direction_vector = parametric_points[1] - parametric_points[0] if direction_vector.is_colinear_to(design3d.X2D, 1e-2): end.y = point_before_end.y parametric_points[-1] = end fixed = True elif direction_vector.is_colinear_to(design3d.Y2D, 1e-2): end.x = point_before_end.x parametric_points[-1] = end fixed = True else: fixed = False if (start_flag or end_flag) and not fixed: parametric_points = self.fix_start_end_singularity_point_any_direction(edge3d, parametric_points, discretization_points, tol) return parametric_points def _edge3d_to_2d(self, edge3d, discretization_points, interpolation_degree, parametric_points, tol: float = 1e-6): """Helper function to get the parametric representation of a 3D edge on the BSpline surface.""" if self.u_closed or self.v_closed: parametric_points = self._fix_start_end_singularity_point_at_parametric_domain(edge3d, parametric_points, discretization_points, tol) if self.x_periodicity: parametric_points = self._repair_periodic_boundary_points(edge3d, parametric_points, 'x') if self.y_periodicity: parametric_points = self._repair_periodic_boundary_points(edge3d, parametric_points, 'y') if self._is_line_segment(parametric_points): return [edges.LineSegment2D(parametric_points[0], parametric_points[-1])] parametric_points = verify_repeated_parametric_points(parametric_points) if interpolation_degree >= len(parametric_points): interpolation_degree = len(parametric_points) - 1 if len(parametric_points) > 1 and interpolation_degree > 1: brep = edges.BSplineCurve2D.from_points_interpolation(points=parametric_points, degree=interpolation_degree) if brep: return [brep] return None
[docs] def bsplinecurve3d_to_2d(self, bspline_curve3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ lth = bspline_curve3d.length() if lth <= 1e-6: print('BSplineCurve3D skipped because it is too small') return None n = min(bspline_curve3d.ctrlpts.shape[0], 20) points3d = bspline_curve3d.discretization_points(number_points=n) tol = 1e-6 if lth > 5e-4 else 1e-7 # todo: how to ensure convergence of point3d_to_2d ? points = [self.point3d_to_2d(point3d, tol) for point3d in points3d] if len(points) < 2: return None return self._edge3d_to_2d(bspline_curve3d, points3d, bspline_curve3d.degree, points)
[docs] def fullarcellipse3d_to_2d(self, fullarcellipse3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ number_points = max(self.nb_u, self.nb_v) degree = max(self.degree_u, self.degree_v) tol = 1e-6 if fullarcellipse3d.length() > 1e-5 else 1e-7 points3d = fullarcellipse3d.discretization_points(number_points=number_points) # todo: how to ensure convergence of point3d_to_2d ? points = [self.point3d_to_2d(point3d, tol) for point3d in points3d] return self._edge3d_to_2d(fullarcellipse3d, points3d, degree, points)
@staticmethod def _is_line_segment(points): """Helper function to check if the BREP can be a line segment.""" if points[0].is_close(points[-1]): return False linesegment = edges.LineSegment2D(points[0], points[-1]) for point in points: if not linesegment.point_belongs(point, abs_tol=1e-2): return False return True
[docs] def bsplinecurve2d_to_3d(self, bspline_curve2d): """ Converts the parametric boundary representation into a 3D primitive. """ if bspline_curve2d.name == "parametric.arc": start = self.point2d_to_3d(bspline_curve2d.start) interior = self.point2d_to_3d(bspline_curve2d.evaluate_single(0.5)) end = self.point2d_to_3d(bspline_curve2d.end) vector_u1 = interior - start vector_u2 = interior - end dot_product = vector_u2.dot(vector_u1) if dot_product and abs(dot_product) != 1.0: return [edges.Arc3D.from_3_points(start, interior, end)] number_points = len(bspline_curve2d.control_points) points = [] for point in bspline_curve2d.discretization_points(number_points=number_points): point3d = self.point2d_to_3d(point) if not point3d.in_list(points): points.append(point3d) if len(points) < bspline_curve2d.degree + 1: return None return [edges.BSplineCurve3D.from_points_interpolation(points, bspline_curve2d.degree, centripetal=True)]
[docs] def arc3d_to_2d(self, arc3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ number_points = max(self.nb_u, self.nb_v) degree = min(self.degree_u, self.degree_v) points = [] tol = 1e-6 if arc3d.length() > 1e-5 else 1e-8 for point3d in arc3d.discretization_points(number_points=number_points): point2d = self.point3d_to_2d(point3d, tol) if not point2d.in_list(points): points.append(point2d) start = points[0] end = points[-1] min_bound_x, max_bound_x, min_bound_y, max_bound_y = self.domain if self.x_periodicity: points = self._repair_periodic_boundary_points(arc3d, points, 'x') start = points[0] end = points[-1] if start.is_close(end): if math.isclose(start.x, min_bound_x, abs_tol=1e-4): end.x = max_bound_x else: end.x = min_bound_x if self.y_periodicity: points = self._repair_periodic_boundary_points(arc3d, points, 'y') start = points[0] end = points[-1] if start.is_close(end): if math.isclose(start.y, min_bound_y, abs_tol=1e-4): end.y = max_bound_y else: end.y = min_bound_y if start.is_close(end): return [] linesegment = edges.LineSegment2D(start, end, name="parametric.arc") flag = True for point in points: if not linesegment.point_belongs(point): flag = False break if flag: return [linesegment] if degree > len(points) - 1: degree = len(points) - 1 return [edges.BSplineCurve2D.from_points_interpolation(points, degree, name="parametric.arc")]
[docs] def arcellipse3d_to_2d(self, arcellipse3d): """ Converts the primitive from 3D spatial coordinates to its equivalent 2D primitive in the parametric space. """ # todo: Is this right? Needs detailed investigation number_points = max(self.nb_u, self.nb_v) degree = max(self.degree_u, self.degree_v) points3d = arcellipse3d.discretization_points(number_points=number_points) tol = 1e-6 if arcellipse3d.length() > 1e-5 else 1e-7 points = [self.point3d_to_2d(point3d, tol) for point3d in points3d] return self._edge3d_to_2d(arcellipse3d, points3d, degree, points)
[docs] def arc2d_to_3d(self, arc2d): """Evaluates the Euclidean form for the parametric arc.""" number_points = math.ceil(arc2d.angle * 7) + 1 # 7 points per radian length = arc2d.length() points = [self.point2d_to_3d(arc2d.point_at_abscissa(i * length / (number_points - 1))) for i in range(number_points)] return [edges.BSplineCurve3D.from_points_interpolation( points, max(self.degree_u, self.degree_v), centripetal=True)]
[docs] def rectangular_cut(self, u1: float, u2: float, v1: float, v2: float, name: str = ''): """Deprecated method, Use BSplineFace3D from_surface_rectangular_cut method.""" raise AttributeError("BSplineSurface3D.rectangular_cut is deprecated." " Use the class_method from_surface_rectangular_cut in BSplineFace3D instead")
[docs] def rotation(self, center: design3d.Vector3D, axis: design3d.Vector3D, angle: float): """ BSplineSurface3D rotation. :param center: rotation center :param axis: rotation axis :param angle: angle rotation :return: a new rotated BSplineSurface3D """ new_control_points = [p.rotation(center, axis, angle) for p in self.control_points] new_bsplinesurface3d = BSplineSurface3D(self.degree_u, self.degree_v, new_control_points, self.nb_u, self.nb_v, self.u_multiplicities, self.v_multiplicities, self.u_knots, self.v_knots, self.weights, self.name) return new_bsplinesurface3d
[docs] def translation(self, offset: design3d.Vector3D): """ BSplineSurface3D translation. :param offset: translation vector :return: A new translated BSplineSurface3D """ new_control_points = [p.translation(offset) for p in self.control_points] new_bsplinesurface3d = BSplineSurface3D(self.degree_u, self.degree_v, new_control_points, self.nb_u, self.nb_v, self.u_multiplicities, self.v_multiplicities, self.u_knots, self.v_knots, self.weights, self.name) return new_bsplinesurface3d
[docs] def frame_mapping(self, frame: design3d.Frame3D, side: str): """ Changes frame_mapping and return a new BSplineSurface3D. side = 'old' or 'new' """ new_control_points = [p.frame_mapping(frame, side) for p in self.control_points] new_bsplinesurface3d = BSplineSurface3D(self.degree_u, self.degree_v, new_control_points, self.nb_u, self.nb_v, self.u_multiplicities, self.v_multiplicities, self.u_knots, self.v_knots, self.weights, self.name) return new_bsplinesurface3d
[docs] def plot(self, ax=None, edge_style: EdgeStyle = EdgeStyle(color='grey', alpha=0.5), **kwargs): """Plot representation of the surface.""" u_curves = self.surface_curves['u'] v_curves = self.surface_curves['v'] if ax is None: ax = plt.figure().add_subplot(111, projection='3d') for u in u_curves: u.plot(ax=ax, edge_style=edge_style) for v in v_curves: v.plot(ax=ax, edge_style=edge_style) for point in self.control_points: point.plot(ax, color=edge_style.color, alpha=edge_style.alpha) return ax
[docs] def simplify_surface(self): """ Verifies if BSplineSurface3D could be a Plane3D. :return: A planar surface if possible, otherwise, returns self. """ points = [self.control_points[0]] vector_list = [] for point in self.control_points[1:]: vector = point - points[0] is_colinear = any(vector.is_colinear_to(other_vector) for other_vector in vector_list) if not point.in_list(points) and not is_colinear: points.append(point) vector_list.append(vector) if len(points) == 3: plane3d = Plane3D.from_3_points(*points) if all(plane3d.point_belongs(point) for point in self.control_points): return plane3d break return self
[docs] @classmethod def from_step(cls, arguments, object_dict, **kwargs): """ Converts a step primitive to a BSplineSurface3D. :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 BSplineSurface3D object. :rtype: :class:`design3d.faces.BSplineSurface3D` """ name = arguments[0][1:-1] degree_u = int(arguments[1]) degree_v = int(arguments[2]) points_sets = arguments[3][1:-1].split("),") points_sets = [elem + ")" for elem in points_sets[:-1]] + [ points_sets[-1]] control_points = [] for points_set in points_sets: points = [object_dict[int(i[1:])] for i in points_set[1:-1].split(",")] nb_v = len(points) control_points.extend(points) nb_u = int(len(control_points) / nb_v) u_multiplicities = [int(i) for i in arguments[8][1:-1].split(",")] v_multiplicities = [int(i) for i in arguments[9][1:-1].split(",")] u_knots = [float(i) for i in arguments[10][1:-1].split(",")] v_knots = [float(i) for i in arguments[11][1:-1].split(",")] # knot_spec = arguments[12] if len(arguments) >= 14: weight_data = [ float(i) for i in arguments[13][1:-1].replace("(", "").replace(")", "").split(",") ] else: weight_data = None bsplinesurface = cls(degree_u, degree_v, control_points, nb_u, nb_v, u_multiplicities, v_multiplicities, u_knots, v_knots, weight_data, name) if not bsplinesurface.x_periodicity and not bsplinesurface.y_periodicity: bsplinesurface = bsplinesurface.simplify_surface() return bsplinesurface
[docs] def to_step(self, current_id): """Converts object into a step entity.""" content = '' point_matrix_ids = '(' for points in self.control_points_table: point_ids = '(' for point in points: point_content, point_id = point.to_step(current_id) content += point_content point_ids += f'#{point_id},' current_id = point_id + 1 point_ids = point_ids[:-1] point_ids += '),' point_matrix_ids += point_ids point_matrix_ids = point_matrix_ids[:-1] point_matrix_ids += ')' u_close = '.T.' if self.x_periodicity else '.F.' v_close = '.T.' if self.y_periodicity else '.F.' content += f"#{current_id} = B_SPLINE_SURFACE_WITH_KNOTS('{self.name}',{self.degree_u},{self.degree_v}," \ f"{point_matrix_ids},.UNSPECIFIED.,{u_close},{v_close},.F.,{tuple(self.u_multiplicities)}," \ f"{tuple(self.v_multiplicities)},{tuple(self.u_knots)},{tuple(self.v_knots)},.UNSPECIFIED.);\n" return content, [current_id]
[docs] def grid3d(self, grid2d: grid.Grid2D): """ Generate 3d grid points of a Bspline surface, based on a Grid2D. """ if not self._grids2d: self._grids2d = grid2d points_2d = grid2d.points points_3d = [self.point2d_to_3d(point2d) for point2d in points_2d] return points_3d
[docs] def grid2d_deformed(self, grid2d: grid.Grid2D): """ Dimension and deform a Grid2D points based on a Bspline surface. """ points_2d = grid2d.points points_3d = self.grid3d(grid2d) points_x, points_y = grid2d.points_xy # Parameters index_x = {} # grid point position(i,j), x coordinates position in X(unknown variable) index_y = {} # grid point position(i,j), y coordinates position in X(unknown variable) index_points = {} # grid point position(j,i), point position in points_2d (or points_3d) k_index, p_index = 0, 0 for i in range(0, points_x): for j in range(0, points_y): index_x.update({(j, i): k_index}) index_y.update({(j, i): k_index + 1}) index_points.update({(j, i): p_index}) k_index = k_index + 2 p_index = p_index + 1 equation_points = [] # points combination to compute distances between 2D and 3D grid points for i in range(0, points_y): # row from (0,i) for j in range(1, points_x): equation_points.append(((0, i), (j, i))) for i in range(0, points_x): # column from (i,0) for j in range(1, points_y): equation_points.append(((i, 0), (i, j))) for i in range(0, points_y): # row for j in range(0, points_x - 1): equation_points.append(((j, i), (j + 1, i))) for i in range(0, points_x): # column for j in range(0, points_x - 1): equation_points.append(((i, j), (i, j + 1))) for i in range(0, points_y - 1): # diagonal for j in range(0, points_x - 1): equation_points.append(((j, i), (j + 1, i + 1))) for i in range(0, points_y): # row 2segments (before.point.after) for j in range(1, points_x - 1): equation_points.append(((j - 1, i), (j + 1, i))) for i in range(0, points_x): # column 2segments (before.point.after) for j in range(1, points_y - 1): equation_points.append(((i, j - 1), (i, j + 1))) # geodesic distances between 3D grid points (based on points combination [equation_points]) geodesic_distances = [] for point in equation_points: geodesic_distances.append((self.geodesic_distance( points_3d[index_points[point[0]]], points_3d[index_points[point[1]]])) ** 2) # System of nonlinear equations def non_linear_equations(xparam): vector_f = np.empty(len(equation_points) + 2) idx = 0 for idx, point_ in enumerate(equation_points): vector_f[idx] = abs((xparam[index_x[point_[0]]] ** 2 + xparam[index_x[point_[1]]] ** 2 + xparam[index_y[point_[0]]] ** 2 + xparam[index_y[point_[1]]] ** 2 - 2 * xparam[index_x[point_[0]]] * xparam[index_x[point_[1]]] - 2 * xparam[index_y[point_[0]]] * xparam[index_y[point_[1]]] - geodesic_distances[idx]) / geodesic_distances[idx]) vector_f[idx + 1] = xparam[0] * 1000 vector_f[idx + 2] = xparam[1] * 1000 return vector_f # Solution with "least_squares" x_init = [] # initial guess (2D grid points) for point in points_2d: x_init.append(point[0]) x_init.append(point[1]) z = least_squares(non_linear_equations, x_init) points_2d_deformed = [design3d.Point2D(z.x[i], z.x[i + 1]) for i in range(0, len(z.x), 2)] # deformed 2d grid points grid2d_deformed = grid.Grid2D.from_points(points=points_2d_deformed, points_dim_1=points_x, direction=grid2d.direction) self._grids2d_deformed = grid2d_deformed return points_2d_deformed
[docs] def grid2d_deformation(self, grid2d: grid.Grid2D): """ Compute the deformation/displacement (dx/dy) of a Grid2D based on a Bspline surface. """ if not self._grids2d_deformed: self.grid2d_deformed(grid2d) displacement = self._grids2d_deformed.displacement_compared_to(grid2d) self._displacements = displacement return displacement
[docs] def point2d_parametric_to_dimension(self, point2d: design3d.Point3D, grid2d: grid.Grid2D): """ Convert a point 2d from the parametric to the dimensioned frame. """ # Check if the 0<point2d.x<1 and 0<point2d.y<1 if point2d.x < 0: point2d.x = 0 elif point2d.x > 1: point2d.x = 1 if point2d.y < 0: point2d.y = 0 elif point2d.y > 1: point2d.y = 1 if self._grids2d == grid2d: points_2d = self._grids2d.points else: points_2d = grid2d.points self._grids2d = grid2d if self._displacements is not None: displacement = self._displacements else: displacement = self.grid2d_deformation(grid2d) points_x, points_y = grid2d.points_xy # Parameters index_points = {} # grid point position(j,i), point position in points_2d (or points_3d) p_index = 0 for i in range(0, points_x): for j in range(0, points_y): index_points.update({(j, i): p_index}) p_index = p_index + 1 # Form function "Finite Elements" def form_function(s_param, t_param): empty_n = np.empty(4) empty_n[0] = (1 - s_param) * (1 - t_param) / 4 empty_n[1] = (1 + s_param) * (1 - t_param) / 4 empty_n[2] = (1 + s_param) * (1 + t_param) / 4 empty_n[3] = (1 - s_param) * (1 + t_param) / 4 return empty_n finite_elements_points = [] # 2D grid points index that define one element for j in range(0, points_y - 1): for i in range(0, points_x - 1): finite_elements_points.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1))) finite_elements = [] # finite elements defined with closed polygon for point in finite_elements_points: finite_elements.append( wires.ClosedPolygon2D([points_2d[index_points[point[0]]], points_2d[index_points[point[1]]], points_2d[index_points[point[2]]], points_2d[index_points[point[3]]]])) k = 0 for k, point in enumerate(finite_elements_points): if (wires.Contour2D(finite_elements[k].primitives).point_inside(point2d) or wires.Contour2D(finite_elements[k].primitives).point_belongs(point2d) or ((points_2d[index_points[point[0]]][0] < point2d.x < points_2d[index_points[point[1]]][0]) and point2d.y == points_2d[index_points[point[0]]][1]) or ((points_2d[index_points[point[1]]][1] < point2d.y < points_2d[index_points[point[2]]][1]) and point2d.x == points_2d[index_points[point[1]]][0]) or ((points_2d[index_points[point[3]]][0] < point2d.x < points_2d[index_points[point[2]]][0]) and point2d.y == points_2d[index_points[point[1]]][1]) or ((points_2d[index_points[point[0]]][1] < point2d.y < points_2d[index_points[point[3]]][1]) and point2d.x == points_2d[index_points[point[0]]][0])): break x0 = points_2d[index_points[finite_elements_points[k][0]]][0] y0 = points_2d[index_points[finite_elements_points[k][0]]][1] x1 = points_2d[index_points[finite_elements_points[k][1]]][0] y2 = points_2d[index_points[finite_elements_points[k][2]]][1] x = point2d.x y = point2d.y s_param = 2 * ((x - x0) / (x1 - x0)) - 1 t_param = 2 * ((y - y0) / (y2 - y0)) - 1 n = form_function(s_param, t_param) dx = np.array([displacement[index_points[finite_elements_points[k][0]]][0], displacement[index_points[finite_elements_points[k][1]]][0], displacement[index_points[finite_elements_points[k][2]]][0], displacement[index_points[finite_elements_points[k][3]]][0]]) dy = np.array([displacement[index_points[finite_elements_points[k][0]]][1], displacement[index_points[finite_elements_points[k][1]]][1], displacement[index_points[finite_elements_points[k][2]]][1], displacement[index_points[finite_elements_points[k][3]]][1]]) return design3d.Point2D(point2d.x + np.transpose(n).dot(dx), point2d.y + np.transpose(n).dot(dy))
[docs] def point3d_to_2d_with_dimension(self, point3d: design3d.Point3D, grid2d: grid.Grid2D): """ Compute the point2d of a point3d, on a Bspline surface, in the dimensioned frame. """ point2d = self.point3d_to_2d(point3d) point2d_with_dimension = self.point2d_parametric_to_dimension(point2d, grid2d) return point2d_with_dimension
[docs] def point2d_with_dimension_to_parametric_frame(self, point2d, grid2d: grid.Grid2D): """ Convert a point 2d from the dimensioned to the parametric frame. """ if self._grids2d != grid2d: self._grids2d = grid2d if not self._grids2d_deformed: self.grid2d_deformed(grid2d) points_2d = grid2d.points points_2d_deformed = self._grids2d_deformed.points points_x, points_y = grid2d.points_xy # Parameters index_points = {} # grid point position(j,i), point position in points_2d (or points_3d) p_index = 0 for i in range(0, points_x): for j in range(0, points_y): index_points.update({(j, i): p_index}) p_index = p_index + 1 finite_elements_points = [] # 2D grid points index that define one element for j in range(0, points_y - 1): for i in range(0, points_x - 1): finite_elements_points.append(((i, j), (i + 1, j), (i + 1, j + 1), (i, j + 1))) finite_elements = [] # finite elements defined with closed polygon DEFORMED for point in finite_elements_points: finite_elements.append( wires.ClosedPolygon2D((points_2d_deformed[index_points[point[0]]], points_2d_deformed[index_points[point[1]]], points_2d_deformed[index_points[point[2]]], points_2d_deformed[index_points[point[3]]]))) finite_elements_initial = [] # finite elements defined with closed polygon INITIAL for point in finite_elements_points: finite_elements_initial.append( wires.ClosedPolygon2D((points_2d[index_points[point[0]]], points_2d[index_points[point[1]]], points_2d[index_points[point[2]]], points_2d[index_points[point[3]]]))) k = 0 for k, point in enumerate(finite_elements_points): if (finite_elements[k].point_belongs(point2d) or ((points_2d_deformed[index_points[point[0]]][0] < point2d.x < points_2d_deformed[index_points[point[1]]][0]) and point2d.y == points_2d_deformed[index_points[point[0]]][1]) or ((points_2d_deformed[index_points[finite_elements_points[k][1]]][1] < point2d.y < points_2d_deformed[index_points[finite_elements_points[k][2]]][1]) and point2d.x == points_2d_deformed[index_points[point[1]]][0]) or ((points_2d_deformed[index_points[point[3]]][0] < point2d.x < points_2d_deformed[index_points[point[2]]][0]) and point2d.y == points_2d_deformed[index_points[point[1]]][1]) or ((points_2d_deformed[index_points[point[0]]][1] < point2d.y < points_2d_deformed[index_points[point[3]]][1]) and point2d.x == points_2d_deformed[index_points[point[0]]][0]) or finite_elements[k].primitives[0].point_belongs(point2d) or finite_elements[k].primitives[ 1].point_belongs(point2d) or finite_elements[k].primitives[2].point_belongs(point2d) or finite_elements[k].primitives[ 3].point_belongs(point2d)): break frame_deformed = design3d.Frame2D( finite_elements[k].center_of_mass(), design3d.Vector2D(finite_elements[k].primitives[1].middle_point()[0] - finite_elements[k].center_of_mass()[0], finite_elements[k].primitives[1].middle_point()[1] - finite_elements[k].center_of_mass()[1]), design3d.Vector2D(finite_elements[k].primitives[0].middle_point()[0] - finite_elements[k].center_of_mass()[0], finite_elements[k].primitives[0].middle_point()[1] - finite_elements[k].center_of_mass()[1])) point2d_frame_deformed = design3d.Point2D(point2d.frame_mapping(frame_deformed, 'new')[0], point2d.frame_mapping(frame_deformed, 'new')[1]) frame_inital = design3d.Frame2D( finite_elements_initial[k].center_of_mass(), design3d.Vector2D(finite_elements_initial[k].primitives[1].middle_point()[0] - finite_elements_initial[k].center_of_mass()[0], finite_elements_initial[k].primitives[1].middle_point()[1] - finite_elements_initial[k].center_of_mass()[1]), design3d.Vector2D(finite_elements_initial[k].primitives[0].middle_point()[0] - finite_elements_initial[k].center_of_mass()[0], finite_elements_initial[k].primitives[0].middle_point()[1] - finite_elements_initial[k].center_of_mass()[1])) point2d = point2d_frame_deformed.frame_mapping(frame_inital, 'old') if point2d.x < 0: point2d.x = 0 elif point2d.x > 1: point2d.x = 1 if point2d.y < 0: point2d.y = 0 elif point2d.y > 1: point2d.y = 1 return point2d
[docs] def point2d_with_dimension_to_3d(self, point2d, grid2d: grid.Grid2D): """ Compute the point 3d, on a Bspline surface, of a point 2d define in the dimensioned frame. """ point2d_01 = self.point2d_with_dimension_to_parametric_frame(point2d, grid2d) return self.point2d_to_3d(point2d_01)
[docs] def linesegment2d_parametric_to_dimension(self, linesegment2d, grid2d: grid.Grid2D): """ Convert a linesegment2d from the parametric to the dimensioned frame. """ points = linesegment2d.discretization_points(number_points=20) points_dim = [ self.point2d_parametric_to_dimension( point, grid2d) for point in points] return edges.BSplineCurve2D.from_points_interpolation( points_dim, max(self.degree_u, self.degree_v))
[docs] def linesegment3d_to_2d_with_dimension(self, linesegment3d, grid2d: grid.Grid2D): """ Compute the linesegment2d of a linesegment3d, on a Bspline surface, in the dimensioned frame. """ linesegment2d = self.linesegment3d_to_2d(linesegment3d) bsplinecurve2d_with_dimension = self.linesegment2d_parametric_to_dimension(linesegment2d, grid2d) return bsplinecurve2d_with_dimension
[docs] def linesegment2d_with_dimension_to_parametric_frame(self, linesegment2d): """ Convert a linesegment2d from the dimensioned to the parametric frame. """ try: linesegment2d = edges.LineSegment2D( self.point2d_with_dimension_to_parametric_frame(linesegment2d.start, self._grids2d), self.point2d_with_dimension_to_parametric_frame(linesegment2d.end, self._grids2d)) except NotImplementedError: return None return linesegment2d
[docs] def linesegment2d_with_dimension_to_3d(self, linesegment2d): """ Compute the linesegment3d, on a Bspline surface, of a linesegment2d defined in the dimensioned frame. """ linesegment2d_01 = self.linesegment2d_with_dimension_to_parametric_frame(linesegment2d) linesegment3d = self.linesegment2d_to_3d(linesegment2d_01) return linesegment3d
[docs] def bsplinecurve2d_parametric_to_dimension(self, bsplinecurve2d, grid2d: grid.Grid2D): """ Convert a bsplinecurve2d from the parametric to the dimensioned frame. """ # check if bsplinecurve2d is in a list if isinstance(bsplinecurve2d, list): bsplinecurve2d = bsplinecurve2d[0] points = bsplinecurve2d.control_points points_dim = [] for point in points: points_dim.append(self.point2d_parametric_to_dimension(point, grid2d)) bsplinecurve2d_with_dimension = edges.BSplineCurve2D(bsplinecurve2d.degree, points_dim, bsplinecurve2d.knot_multiplicities, bsplinecurve2d.knots, bsplinecurve2d.weights, bsplinecurve2d.periodic) return bsplinecurve2d_with_dimension
[docs] def bsplinecurve3d_to_2d_with_dimension(self, bsplinecurve3d, grid2d: grid.Grid2D): """ Compute the bsplinecurve2d of a bsplinecurve3d, on a Bspline surface, in the dimensioned frame. """ bsplinecurve2d_01 = self.bsplinecurve3d_to_2d(bsplinecurve3d) bsplinecurve2d_with_dimension = self.bsplinecurve2d_parametric_to_dimension( bsplinecurve2d_01, grid2d) return bsplinecurve2d_with_dimension
[docs] def bsplinecurve2d_with_dimension_to_parametric_frame(self, bsplinecurve2d): """ Convert a bsplinecurve2d from the dimensioned to the parametric frame. """ points_dim = bsplinecurve2d.control_points points = [] for point in points_dim: points.append( self.point2d_with_dimension_to_parametric_frame(point, self._grids2d)) bsplinecurve2d = edges.BSplineCurve2D(bsplinecurve2d.degree, points, bsplinecurve2d.knot_multiplicities, bsplinecurve2d.knots, bsplinecurve2d.weights, bsplinecurve2d.periodic) return bsplinecurve2d
[docs] def bsplinecurve2d_with_dimension_to_3d(self, bsplinecurve2d): """ Compute the bsplinecurve3d, on a Bspline surface, of a bsplinecurve2d defined in the dimensioned frame. """ bsplinecurve2d_01 = self.bsplinecurve2d_with_dimension_to_parametric_frame(bsplinecurve2d) bsplinecurve3d = self.bsplinecurve2d_to_3d(bsplinecurve2d_01) return bsplinecurve3d
[docs] def arc2d_parametric_to_dimension(self, arc2d, grid2d: grid.Grid2D): """ Convert an arc 2d from the parametric to the dimensioned frame. """ number_points = math.ceil(arc2d.angle * 7) + 1 length = arc2d.length() points = [self.point2d_parametric_to_dimension(arc2d.point_at_abscissa( i * length / (number_points - 1)), grid2d) for i in range(number_points)] return edges.BSplineCurve2D.from_points_interpolation( points, max(self.degree_u, self.degree_v))
[docs] def arc3d_to_2d_with_dimension(self, arc3d, grid2d: grid.Grid2D): """ Compute the arc 2d of an arc 3d, on a Bspline surface, in the dimensioned frame. """ bsplinecurve2d = self.arc3d_to_2d(arc3d)[0] # it's a bsplinecurve2d arc2d_with_dimension = self.bsplinecurve2d_parametric_to_dimension(bsplinecurve2d, grid2d) return arc2d_with_dimension # it's a bsplinecurve2d-dimension
[docs] def arc2d_with_dimension_to_parametric_frame(self, arc2d): """ Convert an arc 2d from the dimensioned to the parametric frame. """ number_points = math.ceil(arc2d.angle * 7) + 1 length = arc2d.length() points = [self.point2d_with_dimension_to_parametric_frame(arc2d.point_at_abscissa( i * length / (number_points - 1)), self._grids2d) for i in range(number_points)] return edges.BSplineCurve2D.from_points_interpolation(points, max(self.degree_u, self.degree_v))
[docs] def arc2d_with_dimension_to_3d(self, arc2d): """ Compute the arc 3d, on a Bspline surface, of an arc 2d in the dimensioned frame. """ arc2d_01 = self.arc2d_with_dimension_to_parametric_frame(arc2d) arc3d = self.arc2d_to_3d(arc2d_01) return arc3d # it's a bsplinecurve3d
[docs] def contour2d_parametric_to_dimension(self, contour2d: wires.Contour2D, grid2d: grid.Grid2D): """ Convert a contour 2d from the parametric to the dimensioned frame. """ primitives2d_dim = [] for primitive2d in contour2d.primitives: method_name = f'{primitive2d.__class__.__name__.lower()}_parametric_to_dimension' if hasattr(self, method_name): primitives = getattr(self, method_name)(primitive2d, grid2d) if primitives: primitives2d_dim.append(primitives) else: raise NotImplementedError( f'Class {self.__class__.__name__} does not implement {method_name}') return wires.Contour2D(primitives2d_dim)
[docs] def contour3d_to_2d_with_dimension(self, contour3d: wires.Contour3D, grid2d: grid.Grid2D): """ Compute the Contour 2d of a Contour 3d, on a Bspline surface, in the dimensioned frame. """ contour2d_01 = self.contour3d_to_2d(contour3d) return self.contour2d_parametric_to_dimension(contour2d_01, grid2d)
[docs] def contour2d_with_dimension_to_parametric_frame(self, contour2d): """ Convert a contour 2d from the dimensioned to the parametric frame. """ # TODO: check and avoid primitives with start=end primitives2d = [] for primitive2d in contour2d.primitives: method_name = f'{primitive2d.__class__.__name__.lower()}_with_dimension_to_parametric_frame' if hasattr(self, method_name): primitives = getattr(self, method_name)(primitive2d) if primitives: primitives2d.append(primitives) else: raise NotImplementedError( f'Class {self.__class__.__name__} does not implement {method_name}') # #Avoid to have primitives with start=end # start_points = [] # for i in range(0, len(new_start_points)-1): # if new_start_points[i] != new_start_points[i+1]: # start_points.append(new_start_points[i]) # if new_start_points[-1] != new_start_points[0]: # start_points.append(new_start_points[-1]) return wires.Contour2D(primitives2d)
[docs] def contour2d_with_dimension_to_3d(self, contour2d): """ Compute the contour3d, on a Bspline surface, of a contour2d define in the dimensioned frame. """ contour01 = self.contour2d_with_dimension_to_parametric_frame(contour2d) return self.contour2d_to_3d(contour01)
[docs] @classmethod def from_geomdl_surface(cls, surface, name: str = ""): """ Create a design3d BSpline_Surface3D from a geomdl's one. """ control_points = [] for point in surface.ctrlpts: control_points.append(design3d.Point3D(point[0], point[1], point[2])) (u_knots, u_multiplicities) = knots_vector_inv(surface.knotvector_u) (v_knots, v_multiplicities) = knots_vector_inv(surface.knotvector_v) bspline_surface = cls(degree_u=surface.degree_u, degree_v=surface.degree_v, control_points=control_points, nb_u=surface.ctrlpts_size_u, nb_v=surface.ctrlpts_size_v, u_multiplicities=u_multiplicities, v_multiplicities=v_multiplicities, u_knots=u_knots, v_knots=v_knots, weights=surface.weights, name=name) return bspline_surface
[docs] @classmethod def points_fitting_into_bspline_surface(cls, points_3d, size_u, size_v, degree_u, degree_v, name: str = ""): """ Bspline Surface interpolation through 3d points. """ warnings.warn("points_fitting_into_bspline_surface is deprecated. Use from_points_interpolation instead") return cls.from_points_interpolation(points_3d, size_u, size_v, degree_u, degree_v, name)
[docs] @classmethod def from_points_interpolation(cls, points_3d: List[design3d.Point3D], size_u: int, size_v: int, degree_u: int, degree_v: int, name: str = ""): """ Bspline Surface interpolation through 3d points. :param points_3d: data points. :type points_3d: List[design3d.Point3D] :param size_u: number of data points on the u-direction. :type size_u: int :param size_v: number of data points on the v-direction. :type size_v: int :param degree_u: degree of the output surface for the u-direction. :type degree_u: int :param degree_v: degree of the output surface for the v-direction. :type degree_v: int :param name: (Optional) instance name. :type name: str :return: B-spline surface. :rtype: BSplineSurface3D """ points = np.asarray(points_3d) ctrlpts, knots_u, knot_multiplicities_u, knots_v, knot_multiplicities_v = \ interpolate_surface(points, size_u, size_v, degree_u, degree_v) ctrlpts = [design3d.Point3D(*point) for point in ctrlpts] return cls(degree_u, degree_v, ctrlpts, size_u, size_v, knot_multiplicities_u, knot_multiplicities_v, knots_u, knots_v, name=name)
[docs] @classmethod def points_approximate_into_bspline_surface(cls, points_3d, size_u, size_v, degree_u, degree_v, name: str = "", **kwargs): """ Bspline Surface approximate through 3d points. """ warnings.warn("points_approximate_into_bspline_surface is deprecated. Use from_points_approximation instead") return cls.from_points_approximation(points_3d, size_u, size_v, degree_u, degree_v, name, **kwargs)
[docs] @classmethod def from_points_approximation(cls, points_3d: List[design3d.Point3D], size_u: int, size_v: int, degree_u: int, degree_v: int, name: str = "", **kwargs): """ Bspline Surface approximate through 3d points. :param points_3d: data points. :type points_3d: List[design3d.Point3D] :param size_u: number of data points on the u-direction. :type size_u: int :param size_v: number of data points on the v-direction. :type size_v: int :param degree_u: degree of the output surface for the u-direction. :type degree_u: int :param degree_v: degree of the output surface for the v-direction. :type degree_v: int :param name: (Optional) instance name. :type name: str Keyword Arguments: * ``ctrlpts_size_u``: number of control points on the u-direction. *Default: size_u - 1* * ``ctrlpts_size_v``: number of control points on the v-direction. *Default: size_v - 1* :return: B-spline surface. :rtype: BSplineSurface3D """ # Keyword arguments # number of data points, r + 1 > number of control points, n + 1 num_cpts_u = kwargs.get('ctrlpts_size_u', size_u - 1) # number of data points, s + 1 > number of control points, m + 1 num_cpts_v = kwargs.get('ctrlpts_size_v', size_v - 1) points = np.asarray(points_3d) ctrlpts, knots_u, knot_multiplicities_u, knots_v, knot_multiplicities_v = \ approximate_surface(points, size_u, size_v, degree_u, degree_v, ctrlpts_size_u=num_cpts_u, ctrlpts_size_v=num_cpts_v) ctrlpts = [design3d.Point3D(*point) for point in ctrlpts] return cls(degree_u, degree_v, ctrlpts, size_u, size_v, knot_multiplicities_u, knot_multiplicities_v, knots_u, knots_v, name=name)
@classmethod def _from_cylindrical_faces_x_direction(cls, cylindrical_faces, degree_u, degree_v, points_x: int = 10, points_y: int = 10, name: str = ''): """ Define an x direction bspline surface from a list of cylindrical faces. Parameters ---------- cylindrical_faces : List[design3d.faces.CylindricalFace3D] faces 3d degree_u : int degree of the output surface for the u-direction degree_v : int degree of the output surface for the v-direction points_x : int number of points in x-direction points_y : int number of points in y-direction name: str object's name. Returns ------- B-spline surface """ bspline_surfaces = [] bounding_rectangle_0 = cylindrical_faces[0].surface2d.outer_contour.bounding_rectangle ymin = bounding_rectangle_0[2] ymax = bounding_rectangle_0[3] for face in cylindrical_faces: bounding_rectangle = face.surface2d.outer_contour.bounding_rectangle ymin = min(ymin, bounding_rectangle[2]) ymax = max(ymax, bounding_rectangle[3]) for face in cylindrical_faces: bounding_rectangle = face.surface2d.outer_contour.bounding_rectangle points_3d = face.surface3d.grid3d( grid.Grid2D.from_properties( x_limits=(bounding_rectangle[0], bounding_rectangle[1]), y_limits=(ymin, ymax), points_nbr=(points_x, points_y))) bspline_surfaces.append( cls.points_fitting_into_bspline_surface( points_3d, points_x, points_y, degree_u, degree_v, name)) return bspline_surfaces @classmethod def _from_cylindrical_faces_y_direction(cls, cylindrical_faces, degree_u, degree_v, points_x: int = 10, points_y: int = 10, name: str = ''): """ Define a y direction bspline surface from a list of cylindrical faces. Parameters ---------- cylindrical_faces : List[design3d.faces.CylindricalFace3D] faces 3d degree_u : int degree of the output surface for the u-direction degree_v : int degree of the output surface for the v-direction points_x : int number of points in x-direction points_y : int number of points in y-direction name: str object's name. Returns ------- B-spline surface """ bspline_surfaces = [] bounding_rectangle_0 = cylindrical_faces[0].surface2d.outer_contour.bounding_rectangle xmin = bounding_rectangle_0[0] xmax = bounding_rectangle_0[1] for face in cylindrical_faces: bounding_rectangle = face.surface2d.outer_contour.bounding_rectangle xmin = min(xmin, bounding_rectangle[0]) xmax = max(xmax, bounding_rectangle[1]) for face in cylindrical_faces: bounding_rectangle = face.surface2d.outer_contour.bounding_rectangle points_3d = face.surface3d.grid3d( grid.Grid2D.from_properties( x_limits=(xmin, xmax), y_limits=(bounding_rectangle[2], bounding_rectangle[3]), points_nbr=(points_x, points_y))) bspline_surfaces.append( cls.points_fitting_into_bspline_surface( points_3d, points_x, points_y, degree_u, degree_v, name)) return bspline_surfaces
[docs] @classmethod def from_cylindrical_faces(cls, cylindrical_faces, degree_u, degree_v, points_x: int = 10, points_y: int = 10, name: str = ''): """ Define a bspline surface from a list of cylindrical faces. Parameters ---------- cylindrical_faces : List[design3d.faces.CylindricalFace3D] faces 3d degree_u : int degree of the output surface for the u-direction degree_v : int degree of the output surface for the v-direction points_x : int number of points in x-direction points_y : int number of points in y-direction name: str object's name. Returns ------- B-spline surface """ if len(cylindrical_faces) < 1: raise NotImplementedError if len(cylindrical_faces) == 1: return cls.from_cylindrical_face(cylindrical_faces[0], degree_u, degree_v, points_x=50, points_y=50) bspline_surfaces = [] direction = cylindrical_faces[0].adjacent_direction(cylindrical_faces[1]) if direction == 'x': bspline_surfaces.extend(cls._from_cylindrical_faces_x_direction( cylindrical_faces, degree_u, degree_v, points_x, points_y, name)) elif direction == 'y': bspline_surfaces.extend(cls._from_cylindrical_faces_y_direction( cylindrical_faces, degree_u, degree_v, points_x, points_y, name )) to_be_merged = bspline_surfaces[0] for i in range(0, len(bspline_surfaces) - 1): merged = to_be_merged.merge_with(bspline_surfaces[i + 1]) to_be_merged = merged bspline_surface = to_be_merged bspline_surface.name = name return bspline_surface
[docs] @classmethod def from_cylindrical_face(cls, cylindrical_face, degree_u, degree_v, name: str = '', **kwargs): # points_x: int = 50, points_y: int = 50 """ Define a bspline surface from a cylindrical face. Parameters ---------- cylindrical_face : design3d.faces.CylindricalFace3D face 3d degree_u : int degree of the output surface for the u-direction. degree_v : int degree of the output surface for the v-direction. points_x : int number of points in x-direction points_y : int number of points in y-direction name: str object's name. Returns ------- B-spline surface """ points_x = kwargs['points_x'] points_y = kwargs['points_y'] bounding_rectangle = cylindrical_face.surface2d.outer_contour.bounding_rectangle points_3d = cylindrical_face.surface3d.grid3d( grid.Grid2D.from_properties(x_limits=(bounding_rectangle[0], bounding_rectangle[1]), y_limits=(bounding_rectangle[2], bounding_rectangle[3]), points_nbr=(points_x, points_y))) return cls.points_fitting_into_bspline_surface(points_3d, points_x, points_x, degree_u, degree_v, name=name)
[docs] def intersection_with(self, other_bspline_surface3d): """ Compute intersection points between two Bspline surfaces. return u,v parameters for intersection points for both surfaces """ def fun(param): return (self.point2d_to_3d(design3d.Point2D(param[0], param[1])) - other_bspline_surface3d.point2d_to_3d(design3d.Point2D(param[2], param[3]))).norm() x = np.linspace(0, 1, 10) x_init = [] for xi in x: for yi in x: x_init.append((xi, yi, xi, yi)) u1, v1, u2, v2 = [], [], [], [] solutions = [] for x0 in x_init: z = least_squares(fun, x0=x0, bounds=([0, 1])) if z.fun < 1e-5: solution = z.x if solution not in solutions: solutions.append(solution) u1.append(solution[0]) v1.append(solution[1]) u2.append(solution[2]) v2.append(solution[3]) # uv1 = [[min(u1),max(u1)],[min(v1),max(v1)]] # uv2 = [[min(u2),max(u2)],[min(v2),max(v2)]] return (u1, v1), (u2, v2) # (uv1, uv2)
[docs] def plane_intersections(self, plane3d): """ Compute intersection points between a Bspline surface and a plane 3d. """ a, b, c, d = plane3d.equation_coefficients() def fun(param): point3d = self.point2d_to_3d(design3d.Point2D(*param)) return point3d[0] * a + point3d[1] * b + point3d[2] * c + d x = np.linspace(0, 1, 20) x_init = [] for xi in x: for yi in x: x_init.append((xi, yi)) intersection_points = [] for x0 in x_init: z = least_squares(fun, x0=np.array(x0), bounds=([0, 1])) if abs(z.fun) < 1e-8: solution = z.x intersection_points.append(self.point2d_to_3d(design3d.Point2D(*solution))) return intersection_points
[docs] def error_with_point3d(self, point3d): """ Compute the error/distance between the Bspline surface and a point 3d. """ def fun(x): return (point3d - self.point2d_to_3d(design3d.Point2D(x[0], x[1]))).norm() cost = [] for x0 in [(0, 0), (0, 1), (1, 0), (1, 1), (0.5, 0.5)]: z = least_squares(fun, x0=x0, bounds=([0, 1])) cost.append(z.fun) return min(cost)
[docs] def error_with_edge3d(self, edge3d): """ Compute the error/distance between the Bspline surface and an edge 3d. it's the mean of the start and end points errors' """ return (self.error_with_point3d(edge3d.start) + self.error_with_point3d(edge3d.end)) / 2
[docs] def nearest_edges3d(self, contour3d, threshold: float): """ Compute the nearest edges of a contour 3d to a Bspline_surface3d based on a threshold. """ nearest = [] for primitive in contour3d.primitives: if self.error_with_edge3d(primitive) <= threshold: nearest.append(primitive) nearest_primitives = wires.Wire3D(nearest) return nearest_primitives
[docs] def edge3d_to_2d_with_dimension(self, edge3d, grid2d: grid.Grid2D): """ Compute the edge 2d of an edge 3d, on a Bspline surface, in the dimensioned frame. """ method_name = f'{edge3d.__class__.__name__.lower()}_to_2d_with_dimension' if hasattr(self, method_name): edge2d_dim = getattr(self, method_name)(edge3d, grid2d) if edge2d_dim: return edge2d_dim raise NotImplementedError raise NotImplementedError( f'Class {self.__class__.__name__} does not implement {method_name}')
[docs] def wire3d_to_2d(self, wire3d): """ Compute the 2d of a wire 3d, on a Bspline surface. """ contour = self.contour3d_to_2d(wire3d) return wires.Wire2D(contour.primitives)
[docs] def wire3d_to_2d_with_dimension(self, wire3d): """ Compute the 2d of a wire 3d, on a Bspline surface, in the dimensioned frame. """ contour = self.contour3d_to_2d_with_dimension(wire3d, self._grids2d) return wires.Wire2D(contour.primitives)
[docs] def split_surface_u(self, u: float): """ Splits the surface at the input parametric coordinate on the u-direction. :param u: Parametric coordinate u chosen between 0 and 1 :type u: float :return: Two split surfaces :rtype: List[:class:`design3d.faces.BSplineSurface3D`] """ return split_surface_u(self, u)
[docs] def split_surface_v(self, v: float): """ Splits the surface at the input parametric coordinate on the v-direction. :param v: Parametric coordinate v chosen between 0 and 1 :type v: float :return: Two split surfaces :rtype: List[:class:`design3d.faces.BSplineSurface3D`] """ return split_surface_v(self, v)
[docs] def split_surface_with_bspline_curve(self, bspline_curve3d: edges.BSplineCurve3D): """ Cuts the surface into two pieces with a bspline curve. :param bspline_curve3d: A BSplineCurve3d used for cutting :type bspline_curve3d: :class:`edges.BSplineCurve3D` :return: Two split surfaces :rtype: List[:class:`design3d.faces.BSplineSurface3D`] """ surfaces = [] bspline_curve2d = self.bsplinecurve3d_to_2d(bspline_curve3d)[0] # if type(bspline_curve2d) == list: # points = [bspline_curve2d[0].start] # for edge in bspline_curve2d: # points.append(edge.end) # bspline_curve2d = edges.BSplineCurve2D.from_points_approximation(points, 2, ctrlpts_size = 5) contour = design3d.faces.BSplineFace3D.from_surface_rectangular_cut(self, 0, 1, 0, 1).surface2d.outer_contour contours = contour.cut_by_bspline_curve(bspline_curve2d) du, dv = bspline_curve2d.end - bspline_curve2d.start resolution = 8 for contour in contours: u_min, u_max, v_min, v_max = contour.bounding_rectangle.bounds() if du > dv: delta_u = u_max - u_min nlines_x = int(delta_u * resolution) lines_x = [curves.Line2D(design3d.Point2D(u_min, v_min), design3d.Point2D(u_min, v_max))] for i in range(nlines_x): u = u_min + (i + 1) / (nlines_x + 1) * delta_u lines_x.append(curves.Line2D(design3d.Point2D(u, v_min), design3d.Point2D(u, v_max))) lines_x.append(curves.Line2D(design3d.Point2D(u_max, v_min), design3d.Point2D(u_max, v_max))) lines = lines_x else: delta_v = v_max - v_min nlines_y = int(delta_v * resolution) lines_y = [curves.Line2D(design3d.Point2D(v_min, v_min), design3d.Point2D(v_max, v_min))] for i in range(nlines_y): v = v_min + (i + 1) / (nlines_y + 1) * delta_v lines_y.append(curves.Line2D(design3d.Point2D(v_min, v), design3d.Point2D(v_max, v))) lines_y.append(curves.Line2D(design3d.Point2D(v_min, v_max), design3d.Point2D(v_max, v_max))) lines = lines_y pt0 = design3d.O2D points = [] for line in lines: inter = contour.line_intersections(line) if inter: pt_ = set() for point_intersection in inter: pt_.add(point_intersection[0]) else: raise NotImplementedError pt_ = sorted(pt_, key=pt0.point_distance) pt0 = pt_[0] edge = edges.LineSegment2D(pt_[0], pt_[1]) points.extend(edge.discretization_points(number_points=10)) points3d = [] for point in points: points3d.append(self.point2d_to_3d(point)) size_u, size_v, degree_u, degree_v = 10, 10, self.degree_u, self.degree_v surfaces.append( BSplineSurface3D.points_fitting_into_bspline_surface(points3d, size_u, size_v, degree_u, degree_v)) return surfaces
[docs] def is_intersected_with(self, other_bspline_surface3d): """ Check if the two surfaces are intersected or not. return True, when there are more 50points on the intersection zone. """ def fun(param): return (self.point2d_to_3d(design3d.Point2D(param[0], param[1])) - other_bspline_surface3d.point2d_to_3d(design3d.Point2D(param[2], param[3]))).norm() x = np.linspace(0, 1, 10) x_init = [] for xi in x: for yi in x: x_init.append((xi, yi, xi, yi)) i = 0 for x0 in x_init: z = least_squares(fun, x0=x0, bounds=([0, 1])) if z.fun < 1e-5: i += 1 if i >= 50: return True return False
[docs] def merge_with(self, other_bspline_surface3d, abs_tol: float = 1e-6): """ Merges two adjacent surfaces based on their faces. :param other_bspline_surface3d: Other adjacent surface :type other_bspline_surface3d: :class:`design3d.faces.BSplineSurface3D` :param abs_tol: tolerance. :type abs_tol: float. :return: Merged surface :rtype: :class:`design3d.faces.BSplineSurface3D` """ bspline_face3d = design3d.faces.BSplineFace3D.from_surface_rectangular_cut(self, 0, 1, 0, 1) other_bspline_face3d = design3d.faces.BSplineFace3D.from_surface_rectangular_cut( other_bspline_surface3d, 0, 1, 0, 1) bsplines = [self, other_bspline_surface3d] bsplines_new = bsplines center = [bspline_face3d.surface2d.outer_contour.center_of_mass(), other_bspline_face3d.surface2d.outer_contour.center_of_mass()] grid2d_direction = (bspline_face3d.pair_with(other_bspline_face3d))[1] if (not bspline_face3d.outer_contour3d.is_sharing_primitives_with( other_bspline_face3d.outer_contour3d, abs_tol) and self.is_intersected_with(other_bspline_surface3d)): # find primitives to split with contour1 = bspline_face3d.outer_contour3d contour2 = other_bspline_face3d.outer_contour3d distances = [] for prim1 in contour1.primitives: dis = [] for prim2 in contour2.primitives: point1 = (prim1.start + prim1.end) / 2 point2 = (prim2.start + prim2.end) / 2 dis.append(point1.point_distance(point2)) distances.append(dis) i = distances.index((min(distances))) j = distances[i].index(min(distances[i])) curves_ = [contour2.primitives[j], contour1.primitives[i]] # split surface for i, bspline in enumerate(bsplines): surfaces = bspline.split_surface_with_bspline_curve(curves_[i]) errors = [] for surface in surfaces: errors.append(surface.error_with_point3d(bsplines[i].point2d_to_3d(center[i]))) bsplines_new[i] = surfaces[errors.index(min(errors))] grid2d_direction = ( bsplines_new[0].rectangular_cut( 0, 1, 0, 1).pair_with( bsplines_new[1].rectangular_cut( 0, 1, 0, 1)))[1] # grid3d number_points = 10 points3d = [] is_true = (bspline_face3d.outer_contour3d.is_sharing_primitives_with( other_bspline_face3d.outer_contour3d, abs_tol) or self.is_intersected_with(other_bspline_surface3d)) for i, bspline in enumerate(bsplines_new): grid3d = bspline.grid3d(grid.Grid2D.from_properties(x_limits=(0, 1), y_limits=(0, 1), points_nbr=(number_points, number_points), direction=grid2d_direction[i])) if is_true and i == 1: points3d.extend(grid3d[number_points:number_points * number_points]) else: points3d.extend(grid3d) # fitting size_u, size_v, degree_u, degree_v = (number_points * 2) - 1, number_points, 3, 3 merged_surface = BSplineSurface3D.points_fitting_into_bspline_surface( points3d, size_u, size_v, degree_u, degree_v) return merged_surface
[docs] def xy_limits(self, other_bspline_surface3d): """ Compute x, y limits to define grid2d. """ grid2d_direction = ( self.rectangular_cut( 0, 1, 0, 1).pair_with( other_bspline_surface3d.rectangular_cut( 0, 1, 0, 1)))[1] xmin, xmax, ymin, ymax = [], [], [], [] if grid2d_direction[0][1] == '+y': xmin.append(0) xmax.append(1) ymin.append(0) ymax.append(0.99) elif grid2d_direction[0][1] == '+x': xmin.append(0) xmax.append(0.99) ymin.append(0) ymax.append(1) elif grid2d_direction[0][1] == '-x': xmin.append(0.01) xmax.append(1) ymin.append(0) ymax.append(1) elif grid2d_direction[0][1] == '-y': xmin.append(0) xmax.append(1) ymin.append(0.01) ymax.append(1) xmin.append(0) xmax.append(1) ymin.append(0) ymax.append(1) return xmin, xmax, ymin, ymax
def _determine_contour_params(self, outer_contour_start, outer_contour_end, inner_contour_start, inner_contour_end): """ Helper function. """ u1, v1 = outer_contour_start u2, v2 = outer_contour_end u3, v3 = inner_contour_start u4, v4 = inner_contour_end if self.x_periodicity and self.y_periodicity: raise NotImplementedError if self.x_periodicity: outer_contour_param = [u1, u2] inner_contour_param = [u3, u4] elif self.y_periodicity: outer_contour_param = [v1, v2] inner_contour_param = [v3, v4] else: raise NotImplementedError return outer_contour_param, inner_contour_param
[docs] def connect_contours(self, outer_contour, inner_contours): """ Create connections between contours on parametric domain. :param outer_contour: Outer contour 2D. :type inner_contours: wires.Contour2D :param inner_contours: List of 2D contours. :type inner_contours: list """ new_inner_contours = [] new_outer_contour = outer_contour point1 = outer_contour.primitives[0].start point2 = outer_contour.primitives[-1].end for inner_contour in inner_contours: if not inner_contour.is_ordered(): outer_contour_param, inner_contour_param = self._determine_contour_params( point1, point2, inner_contour.primitives[0].start, inner_contour.primitives[-1].end) outer_contour_direction = outer_contour_param[0] < outer_contour_param[1] inner_contour_direction = inner_contour_param[0] < inner_contour_param[1] if outer_contour_direction == inner_contour_direction: inner_contour = inner_contour.invert() closing_linesegment1 = edges.LineSegment2D(outer_contour.primitives[-1].end, inner_contour.primitives[0].start) closing_linesegment2 = edges.LineSegment2D(inner_contour.primitives[-1].end, outer_contour.primitives[0].start) new_outer_contour_primitives = outer_contour.primitives + [closing_linesegment1] + \ inner_contour.primitives + [closing_linesegment2] new_outer_contour = wires.Contour2D(primitives=new_outer_contour_primitives) new_outer_contour.order_contour(tol=1e-3) else: new_inner_contours.append(inner_contour) return new_outer_contour, new_inner_contours
@staticmethod def _get_overlapping_theta(outer_contour_startend_theta, inner_contour_startend_theta): """ Find overlapping theta domain between two contours on periodical Surfaces. """ oc_xmin_index, outer_contour_xmin = min(enumerate(outer_contour_startend_theta), key=lambda x: x[1]) oc_xmax_index, outer_contour_xman = max(enumerate(outer_contour_startend_theta), key=lambda x: x[1]) inner_contour_xmin = min(inner_contour_startend_theta) inner_contour_xmax = max(inner_contour_startend_theta) # check if tetha3 or theta4 is in [theta1, theta2] interval overlap = outer_contour_xmin <= inner_contour_xmax and outer_contour_xman >= inner_contour_xmin if overlap: if inner_contour_xmin < outer_contour_xmin: overlapping_theta = outer_contour_startend_theta[oc_xmin_index] outer_contour_side = oc_xmin_index side = 0 return overlapping_theta, outer_contour_side, side overlapping_theta = outer_contour_startend_theta[oc_xmax_index] outer_contour_side = oc_xmax_index side = 1 return overlapping_theta, outer_contour_side, side # if not direct intersection -> find intersection at periodicity if inner_contour_xmin < outer_contour_xmin: overlapping_theta = outer_contour_startend_theta[oc_xmin_index] - 2 * math.pi outer_contour_side = oc_xmin_index side = 0 return overlapping_theta, outer_contour_side, side overlapping_theta = outer_contour_startend_theta[oc_xmax_index] + 2 * math.pi outer_contour_side = oc_xmax_index side = 1 return overlapping_theta, outer_contour_side, side
[docs] def to_plane3d(self): """ Converts a Bspline surface3d to a Plane3d. :return: A Plane :rtype: Plane3D """ points_2d = [design3d.Point2D(0.1, 0.1), design3d.Point2D(0.1, 0.8), design3d.Point2D(0.8, 0.5)] points = [self.point2d_to_3d(pt) for pt in points_2d] surface3d = Plane3D.from_3_points(points[0], points[1], points[2]) return surface3d
[docs] def u_closed_lower(self, tol: float = 1e-6): """ Returns True if the surface is close in any of the u boundaries. """ a, b, c, _ = self.domain point_at_a_lower = self.point2d_to_3d(design3d.Point2D(a, c)) point_at_b_lower = self.point2d_to_3d(design3d.Point2D(0.5 * (a + b), c)) if point_at_b_lower.is_close(point_at_a_lower, tol): return True return False
[docs] def u_closed_upper(self, tol: float = 1e-6): """ Returns True if the surface is close in any of the u boundaries. """ a, b, _, d = self.domain point_at_a_upper = self.point2d_to_3d(design3d.Point2D(a, d)) point_at_b_upper = self.point2d_to_3d(design3d.Point2D(0.5 * (a + b), d)) if point_at_b_upper.is_close(point_at_a_upper, tol): return True return False
[docs] def v_closed_lower(self, tol: float = 1e-6): """ Returns True if the surface is close in any of the u boundaries. """ a, _, c, d = self.domain point_at_c_lower = self.point2d_to_3d(design3d.Point2D(a, c)) point_at_d_lower = self.point2d_to_3d(design3d.Point2D(a, 0.5 * (c + d))) if point_at_d_lower.is_close(point_at_c_lower, tol): return True return False
[docs] def v_closed_upper(self, tol: float = 1e-6): """ Returns True if the surface is close in any of the u boundaries. """ _, b, c, d = self.domain point_at_c_upper = self.point2d_to_3d(design3d.Point2D(b, c)) point_at_d_upper = self.point2d_to_3d(design3d.Point2D(b, 0.5 * (c + d))) if point_at_d_upper.is_close(point_at_c_upper, tol): return True return False
@cached_property def u_closed(self): """ Returns True if the surface is close in any of the u boundaries. """ return bool(self.u_closed_lower(tol=1e-7) or self.u_closed_upper(tol=1e-7)) @cached_property def v_closed(self): """ Returns True if the surface is close in any of the u boundaries. """ return bool(self.v_closed_lower(tol=1e-7) or self.v_closed_upper(tol=1e-7))
[docs] def is_singularity_point(self, point, *args, **kwargs): """Returns True if the point belongs to the surface singularity and False otherwise.""" tol = kwargs.get("tol", 1e-6) if not self.u_closed and not self.v_closed: return False u_min, u_max, v_min, v_max = self.domain test_lower = self.point2d_to_3d(design3d.Point2D(u_min, v_min)) test_upper = self.point2d_to_3d(design3d.Point2D(u_max, v_max)) if self.u_closed_lower(tol=tol) and test_lower.is_close(point, tol): return True if self.u_closed_upper(tol=tol) and test_upper.is_close(point, tol): return True if self.v_closed_lower(tol=tol) and test_lower.is_close(point, tol): return True if self.v_closed_upper(tol=tol) and test_upper.is_close(point, tol): return True return False
def _get_singularity_line(self, test_point): """ Helper function to fix_start_end_singularity_point_any_direction. Determines the singularity line, side, and domain boundary for a given test point on the parametric domain. - line: A 2D line representing the singularity line (degenerated line) is a 2D line on UV that correspond to a point in 3D space. - side: A string indicating the side of the singularity line ('u-'/'u+' or 'v-'/'v+'). - domain_bound: The domain boundary value associated with the singularity line. :param test_point: The test 3D point that lies on the singularity. """ a, b, c, d = self.domain line = None _side = None _domain_bound = None if self.u_closed: if (self.u_closed_lower() and test_point.is_close(self.point2d_to_3d(design3d.Point2D(0.5 * (a + b), c)))): line = curves.Line2D(design3d.Point2D(a, c), design3d.Point2D(b, c)) _side = "u-" _domain_bound = c if (self.u_closed_upper() and test_point.is_close(self.point2d_to_3d(design3d.Point2D(0.5 * (a + b), d)))): line = curves.Line2D(design3d.Point2D(a, d), design3d.Point2D(b, d)) _side = "u+" _domain_bound = d else: if (self.v_closed_lower() and test_point.is_close(self.point2d_to_3d(design3d.Point2D(a, 0.5 * (c + d))))): line = curves.Line2D(design3d.Point2D(a, c), design3d.Point2D(a, d)) _side = "v-" _domain_bound = a if (self.v_closed_upper() and test_point.is_close(self.point2d_to_3d(design3d.Point2D(b, 0.5 * (c + d))))): line = curves.Line2D(design3d.Point2D(b, c), design3d.Point2D(b, d)) _side = "v+" _domain_bound = b return line, _side, _domain_bound @staticmethod def _verify_points(points, side, domain_bound, start_end): """ Helper function to fix_start_end_singularity_point_any_direction. Verifies and adjusts the given list of points based on the singularity side and domain boundary. :param points: The list of points to be verified. :param side: A string indicating the side of the singularity line ('u-'/'u+' or 'v-'/'v+'). :param domain_bound: The domain boundary value associated with the singularity line. :param start_end: An integer (0 or 1) indicating whether to process the start or end of the list. :return: Verified and adjusted list of points. """ if side.startswith("u"): i = 1 else: i = 0 indexes = [idx for idx, point in enumerate(points) if point[i] == domain_bound] if len(indexes) == 1 and indexes[0] != len(points) - 1: if start_end == 0: return points[indexes[0]:] return points[:indexes[0] + 1] return points
[docs] def fix_start_end_singularity_point_any_direction(self, edge3d, points, points3d, tol: float = 1e-6): """ Helper function. Uses local discretization and line intersection with the tangent line at the point just before the undefined point on the BREP of the 3D edge to find the real values on parametric domain. """ points = verify_repeated_parametric_points(points) def get_temp_edge2d(_points): if len(_points) == 2: edge2d = edges.LineSegment2D(_points[0], _points[1]) else: edge2d = edges.BSplineCurve2D.from_points_interpolation(_points, 2, centripetal=False) return edge2d umin, umax, d3din, d3dax = self.domain if self.is_singularity_point(points3d[0], tol=tol): singularity_line, side, domain_bound = self._get_singularity_line(points3d[0]) if singularity_line and len(points) >= 3: points = self._verify_points(points, side, domain_bound, 0) if len(points) >= 3: temp_edge2d = get_temp_edge2d(points[1:]) point = find_parametric_point_at_singularity(temp_edge2d, abscissa=0, singularity_line=singularity_line, domain=[umin, umax, d3din, d3dax]) if point and not point.is_close(points[0], 1e-3): points[0] = point if self.is_singularity_point(points3d[-1], tol=tol): singularity_line, side, domain_bound = self._get_singularity_line(points3d[-1]) if singularity_line: points = self._verify_points(points, side, domain_bound, 1) if len(points) >= 3: temp_edge2d = get_temp_edge2d(points[:-1]) point = find_parametric_point_at_singularity(temp_edge2d, abscissa=temp_edge2d.length(), singularity_line=singularity_line, domain=[umin, umax, d3din, d3dax]) if point and not point.is_close(points[-1], 1e-3): points[-1] = point return points
[docs] def is_undefined_brep(self, edge): """Returns True if the edge is contained within the periodicity boundary.""" if isinstance(edge.simplify, edges.LineSegment2D): umin, umax, d3din, d3dax = self.domain if self.x_periodicity and edge.simplify.line.unit_direction_vector().is_colinear_to(design3d.Y2D) \ and (math.isclose(abs(edge.start.x), umin, abs_tol=1e-4) or math.isclose(abs(edge.start.x), umax, abs_tol=1e-4)): if (self.point2d_to_3d( design3d.Point2D(umin, d3din)).is_close(self.point2d_to_3d(design3d.Point2D(umax, d3din))) and self.point2d_to_3d( design3d.Point2D(umin, d3dax)).is_close(self.point2d_to_3d(design3d.Point2D(umax, d3dax)))): return True if self.y_periodicity and edge.simplify.line.unit_direction_vector().is_colinear_to(design3d.X2D) \ and (math.isclose(abs(edge.start.y), d3din, abs_tol=1e-4) or math.isclose(abs(edge.start.y), d3dax, abs_tol=1e-4)): if (self.point2d_to_3d( design3d.Point2D(umin, d3din)).is_close(self.point2d_to_3d(design3d.Point2D(umin, d3dax))) and self.point2d_to_3d( design3d.Point2D(umax, d3din)).is_close(self.point2d_to_3d(design3d.Point2D(umax, d3dax)))): return True return False
[docs] def fix_undefined_brep_with_neighbors(self, edge, previous_edge, next_edge): """Uses neighbors edges to fix edge contained within the periodicity boundary.""" delta_previous = previous_edge.end - edge.start delta_next = next_edge.start - edge.end def translate_brep(periodicity): edge_ = edge if not self.is_undefined_brep(previous_edge) and \ math.isclose(delta_previous.norm(), periodicity, abs_tol=1e-3): edge_ = edge.translation(delta_previous) elif not self.is_undefined_brep(next_edge) and \ math.isclose(delta_next.norm(), periodicity, abs_tol=1e-3): edge_ = edge.translation(delta_next) return edge_ if self.x_periodicity: edge = translate_brep(self.x_periodicity) elif self.y_periodicity: edge = translate_brep(self.y_periodicity) return edge
[docs] class BezierSurface3D(BSplineSurface3D): """ A 3D Bezier surface. :param degree_u: The degree of the Bezier surface in the u-direction. :type degree_u: int :param degree_v: The degree of the Bezier surface in the v-direction. :type degree_v: int :param control_points: A list of lists of control points defining the Bezier surface. :type control_points: List[List[`design3d.Point3D`]] :param nb_u: The number of control points in the u-direction. :type nb_u: int :param nb_v: The number of control points in the v-direction. :type nb_v: int :param name: (Optional) name for the Bezier surface. :type name: str """ def __init__(self, degree_u: int, degree_v: int, control_points: List[List[design3d.Point3D]], nb_u: int, nb_v: int, name=''): u_knots = generate_knot_vector(degree_u, nb_u) v_knots = generate_knot_vector(degree_v, nb_v) u_multiplicities = [1] * len(u_knots) v_multiplicities = [1] * len(v_knots) BSplineSurface3D.__init__(self, degree_u, degree_v, control_points, nb_u, nb_v, u_multiplicities, v_multiplicities, u_knots, v_knots, None, name)