"""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 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 point_inversion_grid_search(self, point3d, acceptable_distance, max_iter: int = 15):
"""
Find the parameters (u, v) of a 3D point on the BSpline surface using a grid search algorithm.
"""
point3d_array = np.asarray(point3d)
u, v, u_start, u_stop, v_start, v_stop, delta_u, delta_v, sample_size_u, sample_size_v, minimal_distance = \
self._point_inversion_initialization(point3d_array)
if minimal_distance <= acceptable_distance:
return (u, v), minimal_distance
datadict = self._helper_point_inversion_grid_search_update_evaluation_data(sample_size_u, sample_size_v)
last_distance = 0.0
count = 0
while minimal_distance > acceptable_distance and count < max_iter:
if count > 0:
u_start, u_stop, v_start, v_stop, sample_size_u, sample_size_v = self._get_grid_bounds(
(u, v), delta_u, delta_v)
if sample_size_u == 1 and sample_size_v == 1:
break
datadict["sample_size"] = [sample_size_u, sample_size_v]
matrix = np.asarray(evaluate_surface(datadict, start=(u_start, v_start), stop=(u_stop, v_stop)),
dtype=np.float64)
index, distance = self._find_index_min(matrix, point3d_array)
u, v, delta_u, delta_v = self._update_parameters([u_start, u_stop, v_start, v_stop], sample_size_u,
sample_size_v, index)
if distance < minimal_distance:
minimal_distance = distance
if minimal_distance < acceptable_distance:
break
if abs(distance - last_distance) < acceptable_distance * 0.01:
break
last_distance = distance
count += 1
return (u, v), minimal_distance
[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 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)