Source code for fibomat.shapes.arc_spline

"""Provides the :class:`ArcSpline` and :class:`ArcSplineCompatible` classes."""
from __future__ import annotations
from typing import Optional, Union, Sequence, Iterable, Protocol, runtime_checkable, Tuple, List, Dict

import numpy as np

from fibomat.linalg import VectorLike, Vector, BoundingBox
from fibomat.shapes.shape import Shape

from fibomat import _libfibomat


[docs]@runtime_checkable class ArcSplineCompatible(Protocol): # pylint: disable=too-few-public-methods """Abstract class can be used to mark ArcSpline compatible shapes."""
[docs] def to_arc_spline(self) -> ArcSpline: """ Transform shape to ArcSpline. Returns: ArcSpline """ raise NotImplementedError
[docs]class ArcSpline(Shape, ArcSplineCompatible): """Class represents a spline containing circular arcs and straight line segments. The spline is C^0, hence, continuous but not differentiable. """
[docs] def __init__( self, arc_spline: Union[_libfibomat.ArcSpline, np.ndarray], is_closed: Optional[bool] = None, description: Optional[str] = None): """ Args: arc_spline (_libfibomat.ArcSpline, np.ndarray): np.ndarray must have shape = (-1, 3) where each point is given by (x, y, bulge). is_closed (bool, optional): if True, the last and first point are connected (potentially with an arc, if the bulge value of the last vertex is nonzero). if False, the bulge value of the last point is ignored. The argumetn must be given if arc_spline is np.ndarray and is ignored if arc_spline is _libfibomat.ArcSpline. description (str, optional): description Raises: ValueError: Raised if arc_spline is np.ndarray but is_closed not given. """ super().__init__(description) if isinstance(arc_spline, _libfibomat.ArcSpline): self._arc_spline = _libfibomat.ArcSpline(arc_spline) else: if is_closed is None: raise ValueError('is_closed must be defined if ArcSpline is build from vertices.') self._arc_spline = _libfibomat.ArcSpline(arc_spline, is_closed)
# self._vertices = np.array(self._arc_spline.vertices) # self._vertices.flags.writeable = False def __copy__(self): return self.__class__(arc_spline=self._arc_spline.clone(), description=self.description) def __deepcopy__(self, memodict): return self.__copy__() def __len__(self) -> int: return self._arc_spline.size # shape.Shape methods
[docs] @classmethod def from_segments(cls, segments: Iterable[ArcSplineCompatible], description: Optional[str] = None): """Build an ArcSpline from connected segments. Args: segments (Iterable[ArcSplineCompatible]): segments. the start and end point of to consecutive segments must be equal. If this also holds for the last and first segment, the curve ist closed. description (str, optional): description. Returns: ArcSpline Raises: RuntimeError: Raised if segments are not connected. """ vertices: List[np.ndarray] = [] for i_seg, seg in enumerate(segments): arc_spline = seg.to_arc_spline() if arc_spline.is_closed and i_seg > 0: raise RuntimeError('Cannot build ArcSpline from segments because some segments are closed.') seg_vertices = np.array(arc_spline.vertices) if vertices: if not np.allclose(vertices[-1][-1, :2], seg_vertices[0, :2]): raise RuntimeError('Segments are not C^0. The distance is {}'.format( np.linalg.norm(vertices[-1][-1, :2] - seg_vertices[0, :2])) ) vertices[-1][-1] = seg_vertices[0] vertices.append(seg_vertices[1:]) else: vertices.append(seg_vertices) conc_vertices: np.ndarray = np.concatenate(vertices) if np.allclose(conc_vertices[0, :2], conc_vertices[-1, :2]): return cls(conc_vertices[:-1], True, description) return cls(conc_vertices, False, description)
[docs] @classmethod def from_shape(cls, segment: ArcSplineCompatible): """Converts a single segment to an ArcSpline. Args: segment (ArcSplineCompatible): segment. Returns: ArcSpline """ return segment.to_arc_spline()
def __repr__(self) -> str: return f'{self.__class__.__name__}(start={self.start}, end={self.end}, description={self.description})' @property def is_closed(self) -> bool: return self._arc_spline.is_closed
[docs] def to_arc_spline(self) -> ArcSpline: return self
# Transformable methods
[docs] def clone(self) -> ArcSpline: return self.__class__(arc_spline=self._arc_spline.clone(), description=self.description, )
[docs] def clone_with_new_description(self, description: Optional[str] = None): """Similar to :meth:`ArcSpline.clone` but set the description the passed description. Args: description (str, optional): description. Returns: ArcSpline """ return self.__class__(arc_spline=self._arc_spline.clone(), description=description)
@property def bounding_box(self) -> BoundingBox: # return BoundingBox(*self._arc_spline.bounding_box) # there is a bug in cavc bounding box calculation for multiple arc segments (cf. examples/fill_lines.py without this hacky code) segments = self.segments if len(segments) > 1: bbox = segments[0].bounding_box for seg in segments[1:]: bbox = bbox.extended(seg.bounding_box) return bbox else: return BoundingBox(*self._arc_spline.bounding_box) @property def center(self) -> Vector: return Vector(self._arc_spline.center) def _impl_translate(self, trans_vec: VectorLike) -> None: self._arc_spline.impl_translate(trans_vec) def _impl_scale(self, fac: float) -> None: self._arc_spline.impl_scale(fac) def _impl_rotate(self, theta: float) -> None: self._arc_spline.impl_rotate(theta) def _impl_mirror(self, mirror_axis: VectorLike) -> None: self._arc_spline.impl_mirror(mirror_axis) # other utility methods @property def start(self) -> Vector: """Start point curve Access: get Returns: Vector """ return Vector(self._arc_spline.start[:2]) @property def end(self) -> Vector: """End point curve Access: get Returns: Vector """ if self._arc_spline.is_closed: return Vector(self._arc_spline.start[:2]) return Vector(self._arc_spline.end[:2]) @property def vertices(self) -> np.ndarray: """Curve vertices. Access: get Returns: np.ndarray """ return np.array(self._arc_spline.vertices) @property def segments(self) -> Sequence[ArcSplineCompatible]: """Return a list of Line and Arc elements representing the curve. .. note:: This method is not bijective with regard to the added shapes. E.g. if the curve was constructed from a Circle, this method will not return a Circle element but two Arcs. Access: get Returns: List[shape.Shape] """ from fibomat.shapes.line import Line # pylint: disable=import-outside-toplevel from fibomat.shapes.arc import Arc # pylint: disable=import-outside-toplevel def _make_segment(start_vertex, end_vertex): if np.isclose(start_vertex[2], 0.): return Line(start_vertex[:2], end_vertex[:2]) return Arc.from_bulge(start_vertex[:2], end_vertex[:2], start_vertex[2]) segments = [] vertices = self.vertices for i, vertex in enumerate(vertices[1:], start=1): segments.append(_make_segment(vertices[i-1], vertex)) if self.is_closed: segments.append(_make_segment(vertices[-1], vertices[0])) return segments @property def arc_spline_impl(self) -> _libfibomat.ArcSpline: return self._arc_spline @property def orientation(self) -> bool: """Orientation of curve. True if curve is counterclockwise. .. note:: This property is only defined for closed curves. Access: get Returns: bool """ return self._arc_spline.orientation @property def length(self) -> float: """Length of curve. Access: get Returns: float """ return self._arc_spline.length
[docs] def contains(self, pos: VectorLike): pos = Vector(pos) return self._arc_spline.contains(pos.x, pos.y)
[docs] def closest_point(self, pos: VectorLike) -> Dict: pos = Vector(pos) res = self._arc_spline.closest_point(pos.x, pos.y) return {'segment': pos[0], 'point': Vector(res[1]), 'distance': res[2]}
[docs] def unit_tangents(self, i_vertex: int) -> Tuple[Optional[Vector], Optional[Vector]]: """Unit tangents at vertex i_vertex. Args: i_vertex (int): vertex index Returns: Tuple[Optional[Vector], Optional[Vector]]: left tangent, right tangent. If any of the two tangents does not exist, the tuple entry will be None. Raises: ValueError: Raised if i_vertex > #segmens. """ from fibomat.shapes.arc import Arc # pylint: disable=import-outside-toplevel if i_vertex < self._arc_spline.size: vertices = self.vertices vertex = vertices[i_vertex] first_tangent = None second_tangent = None if i_vertex != 0 or self.is_closed: vertex_before = vertices[(i_vertex-1) % len(vertices)] if np.isclose(vertex_before[2], 0.): first_tangent = vertex[:2] - vertex_before[:2] first_tangent /= np.linalg.norm(first_tangent) else: arc = Arc.from_bulge(vertex_before[:2], vertex[:2], vertex_before[2]) first_tangent = arc.unit_tangent_end if i_vertex != self._arc_spline.size - 1 or self.is_closed: vertex_next = vertices[(i_vertex+1) % len(vertices)] if np.isclose(vertex[2], 0.): second_tangent = vertex_next[:2] - vertex[:2] second_tangent /= np.linalg.norm(second_tangent) else: arc = Arc.from_bulge(vertex[:2], vertex_next[:2], vertex[2]) second_tangent = arc.unit_tangent_start return first_tangent, second_tangent raise ValueError('i_vertex >= number of segments.')
[docs] def kinks(self) -> List[int]: """Return kinks (non differentiable points) of the spline. Returns: List[int]: vertex indices of kinks. """ tangents = [self.unit_tangents(i) for i in range(len(self.vertices))] kinks = [] if self.is_closed: if not np.allclose(tangents[0][0], tangents[0][1]): kinks.append(0) for i in range(1, len(self.vertices) - 1): if not np.allclose(tangents[i][0], tangents[i][1]): kinks.append(i) if self.is_closed: if not np.allclose(tangents[-1][0], tangents[-1][1]): kinks.append(len(self.vertices) - 1) return kinks
[docs] def segments_at_vertex(self, i_vertex: int) -> Tuple[Optional[Shape], Optional[Shape]]: """Return the segments around the vertex with index i_vertex. Args: i_vertex: vertex index Returns: Tuple[Optional[Shape], Optional[Shape]]: left and right segments. If any of the segments is not defined, the tuple entry will be None. Raises: ValueError: Raised if i_vertex > #segmens. """ from fibomat.shapes.arc import Arc # pylint: disable=import-outside-toplevel from fibomat.shapes.line import Line # pylint: disable=import-outside-toplevel if i_vertex < self._arc_spline.size: vertices = self.vertices vertex = vertices[i_vertex] first_seg = None second_seg = None if i_vertex != 0 or self.is_closed: vertex_before = vertices[(i_vertex-1) % len(vertices)] if np.isclose(vertex_before[2], 0.): first_seg = Line(vertex_before[:2], vertex[:2]) else: first_seg = Arc.from_bulge(vertex_before[:2], vertex[:2], vertex_before[2]) if i_vertex != self._arc_spline.size - 1 or self.is_closed: vertex_next = vertices[(i_vertex+1) % len(vertices)] if np.isclose(vertex[2], 0.): second_seg = Line(vertex[:2], vertex_next[:2]) else: second_seg = Arc.from_bulge(vertex[:2], vertex_next[:2], vertex[2]) return first_seg, second_seg raise ValueError('i_vertex >= number of segments.')
[docs] def reversed(self): """Return a reversed copy of the arc spline Returns: ArcSpline """ clone = self.clone() clone._arc_spline.reverse() # pylint: disable=protected-access return clone