Source code for design3d.primitives2d

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Extended primitives 2D classes.
"""

import math
from typing import List, Dict

import matplotlib.patches

import design3d
from design3d import wires, edges, curves, PATH_ROOT
from design3d.core import EdgeStyle
from design3d.primitives import RoundedLineSegments


[docs] class RoundedLineSegments2D(RoundedLineSegments): """ A class representing a series of rounded line segments in 2D. This class inherits from the `RoundedLineSegments` class, and provides methods to work with rounded line segments in 2D. :param points: The list of points defining the line segments. :type points: List[design3d.Point2D] :param radius: The dictionary mapping segment indices to their respective radii. :type radius:Dict[int, float] :param adapt_radius: Flag indicating whether to adapt the radius based on segment length. Defaults to False. :type adapt_radius: bool, optional :param name: The name of the rounded line segments. Defaults to ''. :type name: str, optional """ line_class = design3d.edges.LineSegment2D arc_class = design3d.edges.Arc2D def __init__(self, points: List[design3d.Point2D], radius: Dict[int, float], adapt_radius: bool = False, reference_path: str = PATH_ROOT, name: str = ''): RoundedLineSegments.__init__(self, points=points, radius=radius, adapt_radius=adapt_radius, reference_path=reference_path, name=name)
[docs] def arc_features(self, point_index: int): """ Returns the arc features for point at index. """ # raise NotImplementedError radius = self.radius[point_index] pt1, pti, pt2 = self.get_points(point_index) # TODO: change to point_distance ------> done point_distance1 = (pt1 - pti).norm() point_distance2 = (pt2 - pti).norm() point_distance3 = (pt1 - pt2).norm() alpha = math.acos( -(point_distance3 ** 2 - point_distance1 ** 2 - point_distance2 ** 2) / (2 * point_distance1 * point_distance2)) / 2. point_distance = radius / math.tan(alpha) u1 = (pt1 - pti) / point_distance1 u2 = (pt2 - pti) / point_distance2 p3 = pti + u1 * point_distance p4 = pti + u2 * point_distance w = (u1 + u2).to_vector() if not w.is_close(design3d.Vector2D(0, 0)): w = w.unit_vector() v1 = u1.deterministic_unit_normal_vector() if v1.dot(w) < 0: v1 = -v1 point_curvature = p3 + v1 * radius point_interior = point_curvature - radius * w return p3, point_interior, p4, point_distance, alpha
[docs] def rotation(self, center: design3d.Point2D, angle: float): """ OpenedRoundedLineSegments2D rotation. :param center: rotation center :param angle: angle rotation :return: a new rotated OpenedRoundedLineSegments2D """ return self.__class__([point.rotation(center, angle) for point in self.points], self.radius, adapt_radius=self.adapt_radius, name=self.name)
[docs] def translation(self, offset: design3d.Vector2D): """ OpenedRoundedLineSegments2D translation. :param offset: translation vector :return: A new translated OpenedRoundedLineSegments2D """ return self.__class__( [point.translation(offset) for point in self.points], self.radius, adapt_radius=self.adapt_radius, name=self.name)
def _helper_offset_points_and_radii(self, vectors, number_points, offset): """ Helper method to get offset points and new radii for offset method. :param vectors: offset vectors. :param number_points: number of points. :param offset: offset. :return: list of offset points and radii. """ offset_vectors = [] new_radii = {} offset_points = [] for i in range((not self.closed), number_points - (not self.closed)): check = False normal_i = vectors[2 * i - 1] + vectors[2 * i] if normal_i.is_close(design3d.Vector2D(0, 0)): normal_i = vectors[2 * i] normal_i = normal_i.normal_vector() offset_vectors.append(normal_i) else: normal_i = normal_i.unit_vector() if normal_i.dot(vectors[2 * i - 1].normal_vector()) > 0: normal_i = - normal_i check = True offset_vectors.append(normal_i) if i in self.radius: if (check and offset > 0) or (not check and offset < 0): new_radius = self.radius[i] + abs(offset) else: new_radius = self.radius[i] - abs(offset) if new_radius > 0: new_radii[i] = new_radius else: if self.adapt_radius: new_radii[i] = 1e-6 normal_vector1 = - vectors[2 * i - 1].normal_vector().unit_vector() normal_vector2 = vectors[2 * i].normal_vector().unit_vector() alpha = math.acos(normal_vector1.dot(normal_vector2)) offset_point = self.points[i] + offset / math.cos(alpha / 2) * offset_vectors[i - (not self.closed)] offset_points.append(offset_point) return offset_points, new_radii
[docs] def offset(self, offset): """ Return a new rounded line segment with the specified offset. This method creates a new rounded line segment by offsetting the current one by a given distance. The offset can be both positive and negative, moving the line segments outward or inward. :param offset: The offset distance for the new rounded line segment. :type offset: float :return: A new RoundedLineSegments2D instance with the specified offset. :rtype: RoundedLineSegments2D """ number_points = len(self.points) vectors = [] for i in range(number_points - 1): v1 = (self.points[i + 1] - self.points[i]).unit_vector() v2 = (self.points[i] - self.points[i + 1]).unit_vector() vectors.extend([v1, v2]) if self.closed: v1 = (self.points[0] - self.points[-1]).unit_vector() v2 = (self.points[-1] - self.points[0]).unit_vector() vectors.extend([v1, v2]) offset_points, new_radii = self._helper_offset_points_and_radii(vectors, number_points, offset) if not self.closed: normal_1 = vectors[0].normal_vector() offset_points.insert(0, self.points[0] + offset * normal_1) n_last = vectors[-1].normal_vector() n_last = - n_last offset_points.append(self.points[-1] + offset * n_last) return self.__class__(offset_points, new_radii, adapt_radius=self.adapt_radius)
def _offset_directive_vector_helper(self, line_indexes): """ Computes the directive vectors between which the offset will be drawn. :param line_indexes: A list of consecutive line indexes. :return: directive vector 1 and 2. """ dir_vec_1 = None dir_vec_2 = None if line_indexes[0] == 0 and not self.closed: pass else: dir_vec_1 = self.points[line_indexes[0]] - self.points[line_indexes[0] - 1] if line_indexes[-1] == len(self.points) - (2 - self.closed): if not self.closed: pass else: dir_vec_2 = design3d.Vector2D((self.points[0] - self.points[1])) elif self.closed and line_indexes[-1] == len(self.points) - 2: dir_vec_2 = design3d.Vector2D( (self.points[line_indexes[-1] + 1] - self.points[0])) else: dir_vec_2 = self.points[line_indexes[-1] + 1] - self.points[line_indexes[-1] + 2] if dir_vec_1 is None: dir_vec_1 = dir_vec_2 if dir_vec_2 is None: dir_vec_2 = dir_vec_1 dir_vec_1 = dir_vec_1.unit_vector() dir_vec_2 = dir_vec_2.unit_vector() return dir_vec_1, dir_vec_2
[docs] def get_offset_normal_vectors(self, line_indexes): """ Gets offset normal vectors. :param line_indexes: :return: A list of consecutive line indexes. """ normal_vectors = [] for index in line_indexes: if index == len(self.points) - 1: normal_vectors.append(design3d.Vector2D( self.points[0] - self.points[index]).normalVector( unit=True)) else: normal_vectors.append( (self.points[index + 1] - self.points[index]).unit_normal_vector()) return normal_vectors
def _helper_get_offset_vectors(self, line_indexes, directive_vector1, directive_vector2): """ Helper method: get offset vectors. """ intersection = design3d.Point2D.line_intersection( curves.Line2D(self.points[line_indexes[0]], self.points[line_indexes[0]] + directive_vector1), curves.Line2D(self.points[line_indexes[-1] + 1], self.points[line_indexes[-1] + 1] + directive_vector2)) vec1 = intersection.point_distance(self.points[line_indexes[0]]) * directive_vector1 vec2 = intersection.point_distance(self.points[line_indexes[-1] + 1]) * directive_vector2 return vec1, vec2
[docs] def get_offset_new_points(self, line_indexes, offset, distance_dir1, distance_dir2, directive_vector1, directive_vector2, normal_vectors): """ Get Offset new points. """ if len(line_indexes) <= 1: return [] vec1, vec2 = self._helper_get_offset_vectors(line_indexes, directive_vector1, directive_vector2) new_points = {line_indexes[0]: self.points[line_indexes[0]] + distance_dir1 * directive_vector1} for i, index in enumerate(line_indexes[1:]): coeff_vec_2 = design3d.Point2D.point_distance( self.points[line_indexes[0]], self.points[index]) / design3d.Point2D.point_distance( self.points[line_indexes[0]], self.points[line_indexes[-1] + 1]) coeff_vec_1 = 1 - coeff_vec_2 if directive_vector1.dot(normal_vectors[i + 1]) < 0: coeff_vec_1 = - coeff_vec_1 if directive_vector2.dot(normal_vectors[i + 1]) < 0: coeff_vec_2 = - coeff_vec_2 index_dir_vector = coeff_vec_1 * vec1 + coeff_vec_2 * vec2 index_dot = index_dir_vector.dot(normal_vectors[i + 1]) new_points[index] = self.points[index] + (offset / index_dot) * index_dir_vector if self.closed and line_indexes[-1] == len(self.points) - 1: new_points[0] = self.points[0] + distance_dir2 * directive_vector2 else: new_points[line_indexes[-1] + 1] = self.points[line_indexes[-1] + 1] + distance_dir2 * directive_vector2 return new_points
[docs] def offset_lines(self, line_indexes, offset): """ Line indexes is a list of consecutive line indexes. These line should all be aligned line_indexes = 0 being the 1st line. if self.close last line_index can be len(self.points)-1 if not, last line_index can be len(self.points)-2 """ new_linesegment2d_points = [] dir_vec_1, dir_vec_2 = self._offset_directive_vector_helper(line_indexes) normal_vectors = self.get_offset_normal_vectors(line_indexes) # ============================================================================= # COMPUTES THE ANGLE BETWEEN THE NORMAL VECTOR OF THE SURFACE TO OFFSET AND # THE DIRECTIVE VECTOR IN ORDER TO SET THE NEW POINT AT THE RIGHT DISTANCE # ============================================================================= dot1 = dir_vec_1.dot(normal_vectors[0]) dot2 = dir_vec_2.dot(normal_vectors[-1]) if math.isclose(dot1, 0, abs_tol=1e-9): # call function considering the line before, because the latter and # the first offset segment are parallel return self.offset_lines([line_indexes[0] - 1] + line_indexes, offset) if math.isclose(dot2, 0, abs_tol=1e-9): # call function considering the line after, because the latter and # the last offset segment are parallel return self.offset_lines(line_indexes + [line_indexes[-1] + 1], offset) distance_dir1 = offset / dot1 distance_dir2 = offset / dot2 new_points = self.get_offset_new_points(line_indexes, offset, distance_dir1, distance_dir2, dir_vec_1, dir_vec_2, normal_vectors) for i, point in enumerate(self.points): if i in new_points: new_linesegment2d_points.append(new_points[i]) else: new_linesegment2d_points.append(point) rls_2d = self.__class__(new_linesegment2d_points, self.radius, adapt_radius=self.adapt_radius) return rls_2d
[docs] class OpenedRoundedLineSegments2D(RoundedLineSegments2D, wires.Wire2D): """ Opened Rounded LineSegment2D class. :param points: Points used to draw the wire. :type points: List of Point2D. :param radius: Radius used to connect different parts of the wire. :type radius: {position1(n): float which is the radius linked the n-1 and n+1 points, position2(n+1):...}. """ def __init__(self, points: List[design3d.Point2D], radius: Dict[int, float], adapt_radius: bool = False, reference_path: str = PATH_ROOT, name: str = ''): RoundedLineSegments2D.__init__(self, points, radius, adapt_radius=adapt_radius, reference_path=reference_path, name='') self.closed = False wires.Wire2D.__init__(self, self._primitives(), reference_path=reference_path, name=name)
[docs] class ClosedRoundedLineSegments2D(RoundedLineSegments2D, wires.Contour2D): """ Defines a polygon with some rounded corners. :param points: Points used to draw the wire :type points: List of Point2D :param radius: Radius used to connect different parts of the wire :type radius: {position1(n): float which is the radius linked the n-1 and n+1 points, position2(n+1):...} """ def __init__(self, points: List[design3d.Point2D], radius: Dict[int, float], adapt_radius: bool = False, reference_path: str = PATH_ROOT, name: str = ''): RoundedLineSegments2D.__init__(self, points, radius, adapt_radius=adapt_radius, reference_path=reference_path, name='') self.closed = True # RoundedLineSegments.__init__(self, points, radius, closed=True, adapt_radius=adapt_radius, name=name) wires.Contour2D.__init__(self, self._primitives(), reference_path=reference_path, name=name)
[docs] def copy(self, deep=True, memo=None): """Returns a copy of the object.""" return self.__class__([point.copy(deep, memo) for point in self.points], self.radius.copy(), self.adapt_radius, name='copy_' + self.name)
[docs] class Measure2D(edges.LineSegment2D): """ Measure 2D class. :param unit: 'mm', 'm' or None. If None, the distance won't be in the label. """ def __init__(self, point1, point2, label='', unit: str = 'mm', type_: str = 'distance'): """ :param unit: 'mm', 'm' or None. If None, the distance won't be in the label. """ # TODO: offset parameter edges.LineSegment2D.__init__(self, point1, point2) self.label = label self.unit = unit self.type_ = type_
[docs] def plot(self, ax, edge_style: EdgeStyle()): """Plots the Measure2D.""" ndigits = 6 x1, y1 = self.start x2, y2 = self.end x_middle, y_middle = 0.5 * (self.start + self.end) distance = self.end.point_distance(self.start) if self.label != '': label = f'{self.label}: ' else: label = '' if self.unit == 'mm': label += f'{round(distance * 1000, ndigits)} mm' else: label += f'{round(distance, ndigits)} m' if self.type_ == 'distance': arrow = matplotlib.patches.FancyArrowPatch((x1, y1), (x2, y2), arrowstyle='<|-|>,head_length=10,head_width=5', shrinkA=0, shrinkB=0, color=edge_style.color) elif self.type_ == 'radius': arrow = matplotlib.patches.FancyArrowPatch((x1, y1), (x2, y2), arrowstyle='-|>,head_length=10,head_width=5', shrinkA=0, shrinkB=0, color=edge_style.color) ax.add_patch(arrow) if x2 - x1 == 0.: theta = 90. else: theta = math.degrees(math.atan((y2 - y1) / (x2 - x1))) ax.text(x_middle, y_middle, label, va='bottom', ha='center', rotation=theta)