diff --git a/src/shapepy/bool2d/boolean.py b/src/shapepy/bool2d/boolean.py index a8a47c6c..afd939c0 100644 --- a/src/shapepy/bool2d/boolean.py +++ b/src/shapepy/bool2d/boolean.py @@ -145,7 +145,7 @@ def pursue_path( if (index_jordan, index_segment) in matrix: break matrix.append((index_jordan, index_segment)) - last_point = segment.ctrlpoints[-1] + last_point = segment(1) possibles = [] for i, jordan in enumerate(jordans): if i == index_jordan: @@ -157,7 +157,7 @@ def pursue_path( continue index_jordan = possibles[0] for j, segj in enumerate(all_segments[index_jordan]): - if segj.ctrlpoints[0] == last_point: + if segj(0) == last_point: index_segment = j break return CyclicContainer(matrix) @@ -221,7 +221,7 @@ def midpoints_one_shape( """ for i, jordan in enumerate(shapea.jordans): - for j, segment in enumerate(jordan.piecewise): + for j, segment in enumerate(jordan.parametrize()): mid_point = segment(Fraction(1, 2)) density = shapeb.density(mid_point) mid_point_in = (density > 0 and closed) or density == 1 diff --git a/src/shapepy/bool2d/primitive.py b/src/shapepy/bool2d/primitive.py index bc4c9eef..20aee53b 100644 --- a/src/shapepy/bool2d/primitive.py +++ b/src/shapepy/bool2d/primitive.py @@ -6,17 +6,15 @@ """ +from __future__ import annotations + import math -from copy import copy from typing import Tuple import numpy as np from ..geometry.factory import FactoryJordan -from ..geometry.jordancurve import JordanCurve -from ..geometry.point import Point2D, cartesian -from ..geometry.segment import Segment -from ..geometry.unparam import USegment +from ..geometry.point import Point2D from ..loggers import debug from ..tools import Is, To from .base import EmptyShape, WholeShape @@ -225,25 +223,5 @@ def circle( raise ValueError if not Is.integer(ndivangle) or ndivangle < 4: raise ValueError - center = To.point(center) - - angle = math.tau / ndivangle - height = np.tan(angle / 2) - - start_point = radius * cartesian(1, 0) - middle_point = radius * cartesian(1, height) - beziers = [] - for _ in range(ndivangle - 1): - end_point = copy(start_point).rotate(angle) - new_bezier = Segment([start_point, middle_point, end_point]) - beziers.append(new_bezier) - start_point = end_point - middle_point = copy(middle_point).rotate(angle) - end_point = beziers[0].ctrlpoints[0] - new_bezier = Segment([start_point, middle_point, end_point]) - beziers.append(new_bezier) - - jordan_curve = JordanCurve(map(USegment, beziers)) - jordan_curve.move(center) - circle = SimpleShape(jordan_curve) - return circle + jordan_curve = FactoryJordan.circle(ndivangle) + return SimpleShape(jordan_curve).scale(radius).move(center) diff --git a/src/shapepy/bool2d/shape.py b/src/shapepy/bool2d/shape.py index 130fdfe0..b7492489 100644 --- a/src/shapepy/bool2d/shape.py +++ b/src/shapepy/bool2d/shape.py @@ -45,7 +45,7 @@ def __deepcopy__(self, memo) -> SimpleShape: def __str__(self) -> str: # pragma: no cover # For debug area = float(self.area) - vertices = tuple(map(tuple, self.jordan.vertices)) + vertices = tuple(map(tuple, self.jordan.vertices())) return f"SimpleShape[{area:.2f}]:[{vertices}]" def __eq__(self, other: SubSetR2) -> bool: diff --git a/src/shapepy/geometry/base.py b/src/shapepy/geometry/base.py index 99992d28..5eaf661b 100644 --- a/src/shapepy/geometry/base.py +++ b/src/shapepy/geometry/base.py @@ -67,7 +67,6 @@ def parametrize(self) -> IParametrizedCurve: def __or__(self, other: IGeometricCurve) -> IGeometricCurve: return Future.concatenate((self, other)) - @abstractmethod def move(self, vector: Point2D) -> IGeometricCurve: """ Moves/translate entire shape by an amount diff --git a/src/shapepy/geometry/concatenate.py b/src/shapepy/geometry/concatenate.py index 0279ff2d..72cb20b5 100644 --- a/src/shapepy/geometry/concatenate.py +++ b/src/shapepy/geometry/concatenate.py @@ -4,63 +4,67 @@ from typing import Iterable, Union -import pynurbs - -from ..tools import Is, NotExpectedError, To -from .base import IParametrizedCurve +from ..analytic import Bezier +from ..tools import Is, NotExpectedError +from .base import IGeometricCurve from .piecewise import PiecewiseCurve -from .point import cross, inner +from .point import cross from .segment import Segment +from .unparam import UPiecewiseCurve, USegment -def concatenate(curves: Iterable[IParametrizedCurve]) -> IParametrizedCurve: +def concatenate(curves: Iterable[IGeometricCurve]) -> IGeometricCurve: """ Concatenates the given curves. Ignores all the curves parametrization """ curves = tuple(curves) - if not all(Is.instance(curve, IParametrizedCurve) for curve in curves): + if not all(Is.instance(curve, IGeometricCurve) for curve in curves): raise ValueError - # Check if the curves are connected - for i, curvei in enumerate(curves[:-1]): - curvej = curves[i + 1] - if curvei(curvei.knots[-1]) != curvej(curvej.knots[0]): - raise ValueError("The curves to concatenate are not connected") - segments = [] - for curve in curves: - if Is.instance(curve, Segment): - segments.append(curve) - elif Is.instance(curve, PiecewiseCurve): - segments += list(curve) - else: - raise NotExpectedError(f"Unknown type: {type(curve)}") - return concatenate_segments(segments) + if all(Is.instance(curve, Segment) for curve in curves): + return concatenate_segments(curves) + if all(Is.instance(curve, USegment) for curve in curves): + return concatenate_usegments(curves) + raise NotExpectedError(str(tuple(str(type(c)) for c in curves))) -def concatenate_segments(segments: Iterable[Segment]) -> IParametrizedCurve: +def concatenate_usegments( + usegments: Iterable[USegment], +) -> Union[USegment, UPiecewiseCurve]: + """ + Concatenates all the unparametrized segments + """ + usegments = tuple(usegments) + assert all(Is.instance(useg, USegment) for useg in usegments) + union = concatenate_segments(useg.parametrize() for useg in usegments) + return ( + USegment(union) + if Is.instance(union, Segment) + else UPiecewiseCurve(map(USegment, union)) + ) + + +def concatenate_segments( + segments: Iterable[Segment], +) -> Union[Segment, PiecewiseCurve]: """ Concatenates all the segments """ - segments = list(segments) - assert all(map(Is.segment, segments)) + segments = tuple(segments) + if len(segments) == 0: + raise ValueError(f"Number sizes: {len(segments)}") filtsegments = [] - segment0 = segments.pop(0) - while len(segments) > 0: - segment1 = segments.pop(0) + segments = iter(segments) + segmenti = next(segments) + for segmentj in segments: try: - segment0 = bezier_and_bezier(segment0, segment1) + segmenti = bezier_and_bezier(segmenti, segmentj) except ValueError: - filtsegments.append(segment0) - segment0 = segment1 - filtsegments.append(segment0) - try: - union = bezier_and_bezier(filtsegments[-1], filtsegments[0]) - filtsegments.pop(0) - filtsegments.pop() - filtsegments.append(union) - except ValueError: - pass + filtsegments.append(segmenti) + segmenti = segmentj + filtsegments.append(segmenti) + print("filtsegments = ", filtsegments) return ( PiecewiseCurve(filtsegments) if len(filtsegments) > 1 @@ -68,39 +72,25 @@ def concatenate_segments(segments: Iterable[Segment]) -> IParametrizedCurve: ) -def bezier_and_bezier( - curvea: Segment, curveb: Segment -) -> Union[Segment, PiecewiseCurve]: +def bezier_and_bezier(curvea: Segment, curveb: Segment) -> Segment: """Computes the union of two bezier curves""" - assert Is.instance(curvea, Segment) - assert Is.instance(curveb, Segment) - assert curvea.degree == curveb.degree - if curvea.ctrlpoints[-1] != curveb.ctrlpoints[0]: + if not Is.instance(curvea, Segment): + raise TypeError(f"Invalid type: {type(curvea)}") + if not Is.instance(curveb, Segment): + raise TypeError(f"Invalid type: {type(curveb)}") + if abs(cross(curvea(1, 1), curveb(0, 1))) > 1e-6: raise ValueError - # Last point of first derivative - dapt = curvea.ctrlpoints[-1] - curvea.ctrlpoints[-2] - # First point of first derivative - dbpt = curveb.ctrlpoints[1] - curveb.ctrlpoints[0] - if abs(cross(dapt, dbpt)) > 1e-6: - node = To.rational(1, 2) - else: - dsumpt = dapt + dbpt - denomin = inner(dsumpt, dsumpt) - node = inner(dapt, dsumpt) / denomin - knotvectora = pynurbs.GeneratorKnotVector.bezier( - curvea.degree, To.rational - ) - knotvectora.scale(node) - knotvectorb = pynurbs.GeneratorKnotVector.bezier( - curveb.degree, To.rational - ) - knotvectorb.scale(1 - node).shift(node) - newknotvector = tuple(knotvectora) + tuple( - knotvectorb[curvea.degree + 1 :] - ) - finalcurve = pynurbs.Curve(newknotvector) - finalcurve.ctrlpoints = tuple(curvea.ctrlpoints) + tuple(curveb.ctrlpoints) - finalcurve.knot_clean((node,)) - if finalcurve.degree + 1 != finalcurve.npts: - raise ValueError("Union is not a bezier curve!") - return Segment(finalcurve.ctrlpoints) + if curvea.xfunc.degree != curveb.xfunc.degree: + raise ValueError + if curvea.yfunc.degree != curveb.yfunc.degree: + raise ValueError + if curvea.xfunc.degree > 1 or curvea.yfunc.degree > 1: + raise ValueError + + if curvea(1) != curveb(0): + raise ValueError + startpoint = curvea(0) + endpoint = curveb(1) + nxfunc = Bezier((startpoint[0], endpoint[0])) + nyfunc = Bezier((startpoint[1], endpoint[1])) + return Segment(nxfunc, nyfunc) diff --git a/src/shapepy/geometry/factory.py b/src/shapepy/geometry/factory.py index 500340c3..b3aeb52a 100644 --- a/src/shapepy/geometry/factory.py +++ b/src/shapepy/geometry/factory.py @@ -2,16 +2,45 @@ Defines the Factory to create Jordan Curves """ -from typing import Tuple +from __future__ import annotations +import math +from copy import copy +from typing import Iterable, Tuple + +import numpy as np + +from ..analytic import Bezier from ..loggers import debug from ..tools import To from .jordancurve import JordanCurve -from .point import Point2D -from .segment import Segment, clean_segment +from .point import Point2D, cartesian +from .segment import Segment from .unparam import USegment +# pylint: disable=too-few-public-methods +class FactorySegment: + """ + Define functions to create Segments + """ + + @staticmethod + @debug("shapepy.geometry.factory") + def bezier(ctrlpoints: Iterable[Point2D]) -> Segment: + """Initialize a bezier segment from a list of control points + + :param ctrlpoints: The list of control points + :type ctrlpoints: Iterable[Point2D] + :return: The created segment + :rtype: Segment + """ + ctrlpoints = tuple(map(To.point, ctrlpoints)) + xfunc = Bezier((pt[0] for pt in ctrlpoints)) + yfunc = Bezier((pt[1] for pt in ctrlpoints)) + return Segment(xfunc, yfunc) + + class FactoryJordan: """ Define functions to create Jordan Curves @@ -43,7 +72,7 @@ def polygon(vertices: Tuple[Point2D, ...]) -> JordanCurve: beziers = [None] * nverts for i in range(nverts): ctrlpoints = vertices[i : i + 2] - new_bezier = Segment(ctrlpoints) + new_bezier = FactorySegment.bezier(ctrlpoints) beziers[i] = USegment(new_bezier) return JordanCurve(beziers) @@ -76,6 +105,28 @@ def spline_curve(spline_curve) -> JordanCurve: """ beziers = spline_curve.split(spline_curve.knots) segments = ( - clean_segment(Segment(bezier.ctrlpoints)) for bezier in beziers + FactorySegment.bezier(bezier.ctrlpoints).clean() + for bezier in beziers ) return JordanCurve(map(USegment, segments)) + + @staticmethod + @debug("shapepy.geometry.factory") + def circle(ndivangle: int): + """Creates a jordan curve that represents a unit circle""" + angle = math.tau / ndivangle + height = np.tan(angle / 2) + + start_point = cartesian(1, 0) + middle_point = cartesian(1, height) + all_ctrlpoints = [] + for _ in range(ndivangle - 1): + end_point = copy(start_point).rotate(angle) + all_ctrlpoints.append([start_point, middle_point, end_point]) + start_point = end_point + middle_point = copy(middle_point).rotate(angle) + end_point = all_ctrlpoints[0][0] + all_ctrlpoints.append([start_point, middle_point, end_point]) + return JordanCurve( + map(USegment, map(FactorySegment.bezier, all_ctrlpoints)) + ) diff --git a/src/shapepy/geometry/intersection.py b/src/shapepy/geometry/intersection.py index 6106fabc..9924b393 100644 --- a/src/shapepy/geometry/intersection.py +++ b/src/shapepy/geometry/intersection.py @@ -214,6 +214,11 @@ def param_and_param( raise NotExpectedError +def segment_is_linear(segment: Segment) -> bool: + """Tells if the segment is a polynomial linear""" + return segment.xfunc.degree <= 1 and segment.yfunc.degree <= 1 + + def segment_and_segment( curvea: Segment, curveb: Segment ) -> Tuple[SubSetR1, SubSetR1]: @@ -224,11 +229,12 @@ def segment_and_segment( return Empty(), Empty() if curvea == curveb: return Interval(0, 1), Interval(0, 1) - if curvea.degree == 1 and curveb.degree == 1: - subseta, subsetb = IntersectionSegments.lines(curvea, curveb) - return subseta, subsetb - usample = list(NodeSampleFactory.closed_linspace(curvea.npts + 3)) - vsample = list(NodeSampleFactory.closed_linspace(curveb.npts + 3)) + if segment_is_linear(curvea) and segment_is_linear(curveb): + return IntersectionSegments.lines(curvea, curveb) + nptsa = max(curvea.xfunc.degree, curvea.yfunc.degree) + 4 + nptsb = max(curveb.xfunc.degree, curveb.yfunc.degree) + 4 + usample = list(NodeSampleFactory.closed_linspace(nptsa)) + vsample = list(NodeSampleFactory.closed_linspace(nptsb)) pairs = [] for ui in usample: pairs += [(ui, vj) for vj in vsample] @@ -264,11 +270,9 @@ class IntersectionSegments: @staticmethod def lines(curvea: Segment, curveb: Segment) -> Tuple[SubSetR1, SubSetR1]: """Finds the intersection of two line segments""" - assert curvea.degree == 1 - assert curveb.degree == 1 empty = Empty() - A0, A1 = curvea.ctrlpoints - B0, B1 = curveb.ctrlpoints + A0, A1 = curvea(0), curvea(1) + B0, B1 = curveb(0), curveb(1) dA = A1 - A0 dB = B1 - B0 B0mA0 = B0 - A0 diff --git a/src/shapepy/geometry/jordancurve.py b/src/shapepy/geometry/jordancurve.py index a201924c..e205d6d2 100644 --- a/src/shapepy/geometry/jordancurve.py +++ b/src/shapepy/geometry/jordancurve.py @@ -7,7 +7,7 @@ from collections import deque from copy import copy -from typing import Iterable, Tuple, Union +from typing import Iterable, Iterator, Tuple, Union from ..common import clean from ..loggers import debug @@ -18,7 +18,7 @@ from .box import Box from .piecewise import PiecewiseCurve from .point import Point2D -from .unparam import USegment, clean_usegment, self_intersect +from .unparam import UPiecewiseCurve, USegment, clean_usegment, self_intersect class JordanCurve(IGeometricCurve): @@ -40,17 +40,17 @@ def __deepcopy__(self, memo) -> JordanCurve: @debug("shapepy.geometry.jordancurve") def move(self, vector: Point2D) -> JordanCurve: - self.__piecewise = self.piecewise.move(vector) + self.__usegments = tuple(useg.move(vector) for useg in self.usegments) return self @debug("shapepy.geometry.jordancurve") def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> JordanCurve: - self.__piecewise = self.piecewise.scale(amount) + self.__usegments = tuple(useg.scale(amount) for useg in self.usegments) return self @debug("shapepy.geometry.jordancurve") def rotate(self, angle: Angle) -> JordanCurve: - self.__piecewise = self.piecewise.rotate(angle) + self.__usegments = tuple(useg.rotate(angle) for useg in self.usegments) return self def box(self) -> Box: @@ -77,7 +77,7 @@ def box(self) -> Box: @property def length(self) -> Real: """The length of the curve""" - return self.piecewise.length + return sum(useg.length for useg in self.usegments) @property def area(self) -> Real: @@ -90,12 +90,12 @@ def parametrize(self) -> PiecewiseCurve: """ Gives the piecewise curve """ - return self.piecewise + return self.__piecewise @property def piecewise(self) -> PiecewiseCurve: """ - Gives the piecewise curve + Gives the internal piecewise curve """ return self.__piecewise @@ -122,10 +122,9 @@ def usegments(self) -> CyclicContainer[USegment]: Planar curve of degree 1 and control points ((0, 0), (4, 0)) """ - return CyclicContainer(map(USegment, self.parametrize())) + return self.__usegments - @property - def vertices(self) -> Tuple[Point2D]: + def vertices(self) -> Iterator[Point2D]: """Vertices Returns in order, all the non-repeted control points from @@ -144,12 +143,7 @@ def vertices(self) -> Tuple[Point2D]: ((0, 0), (4, 0), (0, 3)) """ - vertices = [] - for usegment in self.usegments: - segment = usegment.parametrize() - vertex = segment(segment.knots[0]) - vertices.append(vertex) - return tuple(vertices) + yield from (useg.start_point for useg in self.usegments) @usegments.setter def usegments(self, other: Iterable[USegment]): @@ -158,11 +152,12 @@ def usegments(self, other: Iterable[USegment]): raise ValueError(f"Invalid usegments: {tuple(map(type, other))}") if any(map(self_intersect, usegments)): raise ValueError("Segment must not self intersect") - segments = [useg.parametrize() for useg in usegments] - for segi, segj in pairs(segments): - if segi(1) != segj(0): + for usegi, usegj in pairs(usegments, cyclic=True): + if usegi.end_point != usegj.start_point: raise ValueError("The segments are not continuous") - self.__piecewise = PiecewiseCurve(segments) + self.__usegments = CyclicContainer(usegments) + upiece = UPiecewiseCurve(self.usegments) + self.__piecewise = upiece.parametrize() self.__area = None def __str__(self) -> str: @@ -182,7 +177,7 @@ def __eq__(self, other: JordanCurve) -> bool: if ( self.box() != other.box() or self.length != other.length - or not all(point in self for point in other.vertices) + or not all(point in self for point in other.vertices()) ): return False return clean(self).usegments == clean(other).usegments @@ -238,7 +233,7 @@ def __invert__(self) -> JordanCurve: def __contains__(self, point: Point2D) -> bool: """Tells if the point is on the boundary""" - return point in self.piecewise + return any(point in useg for useg in self.usegments) @debug("shapepy.geometry.jordancurve") diff --git a/src/shapepy/geometry/segment.py b/src/shapepy/geometry/segment.py index 879b6f04..ade51249 100644 --- a/src/shapepy/geometry/segment.py +++ b/src/shapepy/geometry/segment.py @@ -15,14 +15,15 @@ from copy import copy from typing import Iterable, Optional, Tuple, Union +from rbool import Interval, from_any + from ..analytic.base import IAnalytic -from ..analytic.bezier import Bezier from ..analytic.tools import find_minimum from ..loggers import debug from ..scalar.angle import Angle from ..scalar.quadrature import AdaptativeIntegrator, IntegratorFactory from ..scalar.reals import Math, Real -from ..tools import Is, To, vectorize +from ..tools import Is, To, pairs, vectorize from .base import IParametrizedCurve from .box import Box from .point import Point2D, cartesian @@ -34,74 +35,58 @@ class Segment(IParametrizedCurve): that contains a bezier curve inside it """ - def __init__(self, ctrlpoints: Iterable[Point2D]): - if not Is.iterable(ctrlpoints): - raise ValueError("Control points must be iterable") + def __init__(self, xfunc: IAnalytic, yfunc: IAnalytic): + if not Is.instance(xfunc, IAnalytic): + raise TypeError + if not Is.instance(yfunc, IAnalytic): + raise TypeError self.__length = None - self.ctrlpoints = list(map(To.point, ctrlpoints)) self.__knots = (To.rational(0, 1), To.rational(1, 1)) + self.__xfunc = xfunc.clean() + self.__yfunc = yfunc.clean() def __str__(self) -> str: - return f"BS[{len(self.ctrlpoints)-1}:{self(0)}->{self(1)}]" + return f"BS{list(self.knots)}:({self.xfunc}, {self.yfunc})" def __repr__(self) -> str: return str(self) def __eq__(self, other: Segment) -> bool: - if not Is.segment(other): - raise ValueError - if self.npts != other.npts: - return False - for pta, ptb in zip(self.ctrlpoints, other.ctrlpoints): - if pta != ptb: - return False - return True + return ( + Is.instance(other, Segment) + and self.xfunc == other.xfunc + and self.yfunc == other.yfunc + ) @debug("shapepy.geometry.segment") def __contains__(self, point: Point2D) -> bool: point = To.point(point) if point not in self.box(): return False - dist_square = (self.xfunc - point[0]) ** 2 + ( - self.yfunc - point[1] - ) ** 2 + deltax = self.xfunc - point.xcoord + deltay = self.yfunc - point.ycoord + dist_square = deltax * deltax + deltay * deltay return find_minimum(dist_square, [0, 1]) < 1e-12 @vectorize(1, 0) def __call__(self, node: Real, derivate: int = 0) -> Point2D: - planar = To.bezier(self.ctrlpoints) - return planar(node, derivate) + xcoord = self.xfunc(node, derivate) + ycoord = self.yfunc(node, derivate) + return cartesian(xcoord, ycoord) @property def xfunc(self) -> IAnalytic: """ Gives the analytic function x(t) from p(t) = (x(t), y(t)) """ - return To.bezier(pt[0] for pt in self.ctrlpoints) + return self.__xfunc @property def yfunc(self) -> IAnalytic: """ Gives the analytic function y(t) from p(t) = (x(t), y(t)) """ - return To.bezier(pt[1] for pt in self.ctrlpoints) - - @property - def degree(self) -> int: - """ - The degree of the bezier curve - - Degree = 1 -> Linear curve - Degree = 2 -> Quadratic - """ - return self.npts - 1 - - @property - def npts(self) -> int: - """ - The number of control points used by the curve - """ - return len(self.ctrlpoints) + return self.__yfunc @property def length(self) -> Real: @@ -113,57 +98,50 @@ def length(self) -> Real: return self.__length @property - def knots(self) -> Tuple[Real, ...]: + def knots(self) -> Tuple[Real, Real]: return self.__knots - @property - def ctrlpoints(self) -> Tuple[Point2D, ...]: - """ - The control points that defines the planar curve - """ - return self.__ctrlpoints - - @ctrlpoints.setter - def ctrlpoints(self, points: Iterable[Point2D]): - self.__length = None - self.__ctrlpoints = list(map(To.point, points)) - self.__planar = To.bezier(self.ctrlpoints) - def derivate(self, times: Optional[int] = 1) -> Segment: """ Gives the first derivative of the curve """ if not Is.integer(times) or times <= 0: raise ValueError(f"Times must be integer >= 1, not {times}") - planar: IAnalytic = To.bezier(self.ctrlpoints) - newplanar: IAnalytic = planar.derivate(times) - return self.__class__(newplanar) + dxfunc = copy(self.xfunc).derivate(times) + dyfunc = copy(self.yfunc).derivate(times) + return Segment(dxfunc, dyfunc) def box(self) -> Box: """Returns two points which defines the minimal exterior rectangle Returns the pair (A, B) with A[0] <= B[0] and A[1] <= B[1] """ - xmin = min(point[0] for point in self.ctrlpoints) - xmax = max(point[0] for point in self.ctrlpoints) - ymin = min(point[1] for point in self.ctrlpoints) - ymax = max(point[1] for point in self.ctrlpoints) + xmin = find_minimum(self.xfunc, [0, 1]) + xmax = -find_minimum(-self.xfunc, [0, 1]) + ymin = find_minimum(self.yfunc, [0, 1]) + ymax = -find_minimum(-self.yfunc, [0, 1]) return Box(cartesian(xmin, ymin), cartesian(xmax, ymax)) + def clean(self) -> Segment: + """Cleans the segment""" + self.__xfunc = self.__xfunc.clean() + self.__yfunc = self.__yfunc.clean() + return self + def __copy__(self) -> Segment: return self.__deepcopy__(None) def __deepcopy__(self, memo) -> Segment: - ctrlpoints = tuple(copy(point) for point in self.ctrlpoints) - return self.__class__(ctrlpoints) + return Segment(copy(self.xfunc), copy(self.yfunc)) def invert(self) -> Segment: """ Inverts the direction of the curve. If the curve is clockwise, it becomes counterclockwise """ - points = tuple(self.ctrlpoints) - self.ctrlpoints = (points[i] for i in range(self.degree, -1, -1)) + half = To.rational(1, 2) + self.__xfunc = self.__xfunc.shift(-half).scale(-1).shift(half) + self.__yfunc = self.__yfunc.shift(-half).scale(-1).shift(half) return self def split(self, nodes: Iterable[Real]) -> Tuple[Segment, ...]: @@ -172,24 +150,36 @@ def split(self, nodes: Iterable[Real]) -> Tuple[Segment, ...]: """ nodes = (n for n in nodes if self.knots[0] <= n <= self.knots[-1]) nodes = sorted(set(nodes) | set(self.knots)) - segments = [] - for ka, kb in zip(nodes, nodes[1:]): - bezier = Bezier(self.__planar).shift(-ka).scale(1 / (kb - ka)) - segments.append(Segment(bezier)) - return tuple(segments) + return tuple(self.extract([ka, kb]) for ka, kb in pairs(nodes)) + + def extract(self, interval: Interval) -> Segment: + """Extracts a subsegment from the given segment""" + interval = from_any(interval) + if not Is.instance(interval, Interval): + raise TypeError + knota, knotb = interval[0], interval[1] + denom = 1 / (knotb - knota) + nxfunc = copy(self.xfunc).shift(-knota).scale(denom) + nyfunc = copy(self.yfunc).shift(-knota).scale(denom) + return Segment(nxfunc, nyfunc) def move(self, vector: Point2D) -> Segment: vector = To.point(vector) - self.ctrlpoints = (copy(pt).move(vector) for pt in self.ctrlpoints) + self.__xfunc += vector.xcoord + self.__yfunc += vector.ycoord return self def scale(self, amount: Union[Real, Tuple[Real, Real]]) -> Segment: - self.ctrlpoints = (copy(pt).scale(amount) for pt in self.ctrlpoints) + self.__xfunc *= amount if Is.real(amount) else amount[0] + self.__yfunc *= amount if Is.real(amount) else amount[1] return self def rotate(self, angle: Angle) -> Segment: angle = To.angle(angle) - self.ctrlpoints = (copy(pt).rotate(angle) for pt in self.ctrlpoints) + cos, sin = angle.cos(), angle.sin() + xfunc, yfunc = self.xfunc, self.yfunc + self.__xfunc = xfunc * cos - yfunc * sin + self.__yfunc = xfunc * sin + yfunc * cos return self @@ -214,15 +204,6 @@ def function(node): return adaptative.integrate(function, domain) -@debug("shapepy.geometry.segment") -def clean_segment(segment: Segment) -> Segment: - """Reduces at maximum the degree of the bezier curve""" - newplanar = To.bezier(segment.ctrlpoints) - if newplanar.degree == segment.degree: - return segment - return Segment(tuple(newplanar)) - - def is_segment(obj: object) -> bool: """ Checks if the parameter is a Segment diff --git a/src/shapepy/geometry/unparam.py b/src/shapepy/geometry/unparam.py index 8b337858..6a34c41a 100644 --- a/src/shapepy/geometry/unparam.py +++ b/src/shapepy/geometry/unparam.py @@ -5,7 +5,7 @@ from __future__ import annotations from copy import copy -from typing import Tuple, Union +from typing import Iterable, Tuple, Union from ..scalar.angle import Angle from ..scalar.reals import Real @@ -13,7 +13,7 @@ from .base import IGeometricCurve from .box import Box from .piecewise import PiecewiseCurve -from .point import Point2D, cross +from .point import Point2D from .segment import Segment @@ -41,6 +41,16 @@ def length(self) -> Real: """ return self.__segment.length + @property + def start_point(self) -> Point2D: + """Gives the start point of the USegment""" + return self.__segment(0) + + @property + def end_point(self) -> Point2D: + """Gives the end point of the USegment""" + return self.__segment(1) + def box(self) -> Box: """ Gives the box that encloses the curve @@ -58,21 +68,6 @@ def __eq__(self, other: IGeometricCurve) -> bool: segj = other.parametrize() return segi(0) == segj(0) and segi(1) == segj(1) - def __or__(self, other: USegment) -> Union[USegment, PiecewiseCurve]: - if not Is.instance(other, USegment): - raise TypeError - segi = self.parametrize() - segj = other.parametrize() - if segi(1) != segj(0): - raise ValueError("Union is not continous") - if segi.npts == 2 and segj.npts == 2: - # They are linear - if abs(cross(segi(1, 1), segj(0, 1))) < 1e-9: - return USegment( - Segment([segi.ctrlpoints[0], segj.ctrlpoints[1]]) - ) - return PiecewiseCurve([segi, segj]) - def invert(self) -> USegment: """Invert the current curve's orientation, doesn't create a copy @@ -95,6 +90,24 @@ def rotate(self, angle: Angle) -> Segment: return self +class UPiecewiseCurve(IGeometricCurve): + """Equivalent to PiecewiseCurve, but ignores the parametrization""" + + def __init__(self, usegments: Iterable[USegment]): + self.__usegments = tuple(usegments) + + @property + def length(self) -> Real: + raise NotImplementedError + + def box(self) -> Box: + raise NotImplementedError + + def parametrize(self) -> PiecewiseCurve: + """Gives a parametrized curve""" + return PiecewiseCurve(useg.parametrize() for useg in self.__usegments) + + def clean_usegment(usegment: USegment) -> USegment: """Cleans the segment, simplifying the expression""" return usegment @@ -102,4 +115,5 @@ def clean_usegment(usegment: USegment) -> USegment: def self_intersect(usegment: USegment) -> USegment: """Checks if the USegment intersects itself""" - return len(usegment.parametrize().ctrlpoints) > 3 + seg = usegment.parametrize() + return seg.xfunc.degree > 2 and seg.yfunc.degree > 2 diff --git a/src/shapepy/plot/plot.py b/src/shapepy/plot/plot.py index 7227bbd2..685f29ae 100644 --- a/src/shapepy/plot/plot.py +++ b/src/shapepy/plot/plot.py @@ -15,6 +15,7 @@ from shapepy.geometry.jordancurve import JordanCurve from shapepy.geometry.segment import Segment +from ..analytic import Bezier from ..tools import Is Path = matplotlib.path.Path @@ -28,11 +29,15 @@ def patch_segment(segment: Segment): assert Is.instance(segment, Segment) vertices = [] commands = [] - if segment.degree == 1: - vertices.append(segment.ctrlpoints[1]) + xfunc, yfunc = segment.xfunc, segment.yfunc + if xfunc.degree <= 1 and yfunc.degree <= 1: + vertices.append(segment(1)) commands.append(Path.LINETO) - elif segment.degree == 2: - vertices += list(segment.ctrlpoints[1:]) + elif xfunc.degree == 2 and yfunc.degree == 2: + xfunc: Bezier = segment.xfunc + yfunc: Bezier = segment.yfunc + ctrlpoints = tuple(zip(xfunc, yfunc)) + vertices += list(ctrlpoints[1:]) commands += [Path.CURVE3] * 2 return vertices, commands @@ -45,7 +50,7 @@ def path_shape(connected: ConnectedShape) -> Path: commands = [] for jordan in connected.jordans: segments = tuple(useg.parametrize() for useg in jordan.usegments) - vertices.append(segments[0].ctrlpoints[0]) + vertices.append(segments[0](0)) commands.append(Path.MOVETO) for segment in segments: verts, comms = patch_segment(segment) @@ -62,7 +67,7 @@ def path_jordan(jordan: JordanCurve) -> Path: Creates the commands for matplotlib to plot the jordan curve """ segments = tuple(useg.parametrize() for useg in jordan.usegments) - vertices = [segments[0].ctrlpoints[0]] + vertices = [segments[0](0)] commands = [Path.MOVETO] for segment in segments: verts, comms = patch_segment(segment) @@ -176,5 +181,5 @@ def plot_subset(self, shape: SubSetR2, *, kwargs): path, edgecolor=color, facecolor="none", lw=2 ) self.gca().add_patch(patch) - xvals, yvals = zip(*jordan.vertices) + xvals, yvals = zip(*jordan.vertices()) self.gca().scatter(xvals, yvals, color=color, marker=marker) diff --git a/src/shapepy/tools.py b/src/shapepy/tools.py index 31ee44f0..f4441daf 100644 --- a/src/shapepy/tools.py +++ b/src/shapepy/tools.py @@ -128,7 +128,9 @@ def reverse(objs: Iterable[Any]) -> Iterable[Any]: return tuple(objs)[::-1] -def pairs(objs: Iterable[Any]) -> Iterable[Tuple[Any, Any]]: +def pairs( + objs: Iterable[Any], /, *, cyclic: bool = False +) -> Iterable[Tuple[Any, Any]]: """Gives pairs of the objects in sequence Example @@ -138,11 +140,10 @@ def pairs(objs: Iterable[Any]) -> Iterable[Tuple[Any, Any]]: [(0, 1), (1, 2), (2, 3), (3, 0)] """ objs = tuple(objs) - if len(objs) <= 1: - raise ValueError(f"objs = {objs}") if len(objs) > 1: yield from zip(objs, objs[1:]) - yield (objs[-1], objs[0]) + if cyclic: + yield (objs[-1], objs[0]) class NotExpectedError(Exception): diff --git a/tests/bool2d/test_contains.py b/tests/bool2d/test_contains.py index c955c1d0..04194926 100644 --- a/tests/bool2d/test_contains.py +++ b/tests/bool2d/test_contains.py @@ -313,14 +313,14 @@ def test_keep_type(self): square = Primitive.square(side=4) good_types = [] jordan = square.jordan - for vertex in jordan.vertices: + for vertex in jordan.vertices(): good_types.append((type(vertex[0]), type(vertex[0]))) one = Fraction(1) for point in [(0, 0), (1, 2), (one / 2, -one / 2), (1.2, 3.5)]: point in square test_types = [] jordan = square.jordan - for vertex in jordan.vertices: + for vertex in jordan.vertices(): test_types.append((type(vertex[0]), type(vertex[0]))) assert len(test_types) == len(good_types) assert test_types == good_types diff --git a/tests/geometry/test_integral.py b/tests/geometry/test_integral.py index 38c05833..f47600f5 100644 --- a/tests/geometry/test_integral.py +++ b/tests/geometry/test_integral.py @@ -2,9 +2,8 @@ import pytest -from shapepy.geometry.factory import FactoryJordan +from shapepy.geometry.factory import FactoryJordan, FactorySegment from shapepy.geometry.integral import lebesgue_density_jordan -from shapepy.geometry.segment import Segment @pytest.mark.order(15) @@ -27,15 +26,15 @@ def test_begin(): @pytest.mark.dependency(depends=["test_begin"]) def test_segment_length(): points = [(0, 0), (1, 0)] - curve = Segment(points) + curve = FactorySegment.bezier(points) assert abs(curve.length - 1) < 1e-9 points = [(1, 0), (0, 0)] - curve = Segment(points) + curve = FactorySegment.bezier(points) assert abs(curve.length - 1) < 1e-9 points = [(0, 1), (1, 0)] - curve = Segment(points) + curve = FactorySegment.bezier(points) assert abs(curve.length - math.sqrt(2)) < 1e-9 diff --git a/tests/geometry/test_piecewise.py b/tests/geometry/test_piecewise.py index e43d819d..529c87af 100644 --- a/tests/geometry/test_piecewise.py +++ b/tests/geometry/test_piecewise.py @@ -4,8 +4,8 @@ import pytest +from shapepy.geometry.factory import FactorySegment from shapepy.geometry.piecewise import PiecewiseCurve -from shapepy.geometry.segment import Segment @pytest.mark.order(14) @@ -31,7 +31,7 @@ def test_build(): ((0, 1), (0, 0)), ] knots = range(len(points) + 1) - segments = tuple(map(Segment, points)) + segments = tuple(map(FactorySegment.bezier, points)) PiecewiseCurve(segments, knots) @@ -45,7 +45,7 @@ def test_box(): ((0, 1), (0, 0)), ] knots = range(len(points) + 1) - segments = tuple(map(Segment, points)) + segments = tuple(map(FactorySegment.bezier, points)) piecewise = PiecewiseCurve(segments, knots) box = piecewise.box() assert box.lowpt == (0, 0) @@ -62,7 +62,7 @@ def test_evaluate(): ((0, 1), (0, 0)), ] knots = range(len(points) + 1) - segments = tuple(map(Segment, points)) + segments = tuple(map(FactorySegment.bezier, points)) piecewise = PiecewiseCurve(segments, knots) assert piecewise(0) == (0, 0) assert piecewise(1) == (1, 0) @@ -86,7 +86,7 @@ def test_print(): ((0, 1), (0, 0)), ] knots = range(len(points) + 1) - segments = tuple(map(Segment, points)) + segments = tuple(map(FactorySegment.bezier, points)) piecewise = PiecewiseCurve(segments, knots) str(piecewise) repr(piecewise) diff --git a/tests/geometry/test_point.py b/tests/geometry/test_point.py index 55740a97..5ecdd8c4 100644 --- a/tests/geometry/test_point.py +++ b/tests/geometry/test_point.py @@ -260,6 +260,35 @@ def test_addsub(): assert pointa != (12, 5) +@pytest.mark.order(11) +@pytest.mark.timeout(1) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_creation_finite_cartesian", + "test_indexable", + "test_addsub", + ] +) +def test_transformations(): + pointa = cartesian(0, 0) + pointb = pointa.move((1, 3)) + assert id(pointb) == id(pointa) + assert pointa == (1, 3) + + pointb = pointa.scale(2) + assert id(pointb) == id(pointa) + assert pointa == (2, 6) + pointb = pointa.scale((5, 3)) + assert id(pointb) == id(pointa) + assert pointa == (10, 18) + + angle = Angle.degrees(90) + pointb = pointa.rotate(angle) + assert id(pointb) == id(pointa) + assert pointa == (-18, 10) + + @pytest.mark.order(11) @pytest.mark.timeout(1) @pytest.mark.dependency( @@ -393,6 +422,7 @@ def test_print(): "test_error_creation", "test_indexable", "test_addsub", + "test_transformations", "test_inner", "test_norm", "test_cross", diff --git a/tests/geometry/test_segment.py b/tests/geometry/test_segment.py index 016516ef..6d4bcbdc 100644 --- a/tests/geometry/test_segment.py +++ b/tests/geometry/test_segment.py @@ -6,7 +6,7 @@ import pytest -from shapepy.geometry.segment import Segment, clean_segment +from shapepy.geometry.factory import FactorySegment @pytest.mark.order(13) @@ -31,7 +31,7 @@ def test_begin(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_build(): - Segment([(0, 0), (1, 0), (0, 1)]) + FactorySegment.bezier([(0, 0), (1, 0), (0, 1)]) class TestDerivate: @@ -50,7 +50,7 @@ def test_begin(self): @pytest.mark.dependency(depends=["TestDerivate::test_begin"]) def test_planar_bezier(self): points = [(0, 0), (1, 0), (0, 1)] - curve = Segment(points) + curve = FactorySegment.bezier(points) dcurve = curve.derivate() assert id(dcurve) != id(curve) @@ -66,68 +66,6 @@ def test_end(self): pass -class TestOperations: - @pytest.mark.order(13) - @pytest.mark.dependency(depends=["test_begin"]) - def test_begin(self): - pass - - @pytest.mark.order(13) - @pytest.mark.timeout(10) - @pytest.mark.dependency(depends=["TestOperations::test_begin"]) - def test_clean_segment(self): - points = [(0, 0), (1, 0)] - curve = clean_segment(Segment(points)) - assert curve.degree == 1 - - points = [(2, 3), (-1, 4)] - curve = clean_segment(Segment(points)) - assert curve.degree == 1 - - points = [(2, 3), (-1, 4)] - curve = clean_segment(Segment(points)) - assert curve.degree == 1 - - @pytest.mark.order(13) - @pytest.mark.timeout(10) - @pytest.mark.dependency( - depends=[ - "TestOperations::test_begin", - "TestOperations::test_clean_segment", - ] - ) - def test_clean_quadratic(self): - points = [(0, 0), (1, 0), (2, 0)] - curve = Segment(points) - assert curve.degree == 2 - curve = clean_segment(curve) - assert curve.degree == 1 - - points = [(0, 2), (1, 4), (2, 6)] - curve = Segment(points) - assert curve.degree == 2 - curve = clean_segment(curve) - assert curve.degree == 1 - - points = [(2, 3), (-1, 4), (-4, 5)] - curve = Segment(points) - assert curve.degree == 2 - curve = clean_segment(curve) - assert curve.degree == 1 - - @pytest.mark.order(13) - @pytest.mark.timeout(10) - @pytest.mark.dependency( - depends=[ - "TestOperations::test_begin", - "TestOperations::test_clean_segment", - "TestOperations::test_clean_quadratic", - ] - ) - def test_end(self): - pass - - class TestContains: @pytest.mark.order(13) @pytest.mark.dependency(depends=["test_begin"]) @@ -139,7 +77,7 @@ def test_begin(self): @pytest.mark.dependency(depends=["TestContains::test_begin"]) def test_line(self): points = [(0, 0), (1, 0)] - curve = Segment(points) + curve = FactorySegment.bezier(points) assert (0, 0) in curve assert (1, 0) in curve assert (0.5, 0) in curve @@ -147,7 +85,7 @@ def test_line(self): assert (0, -1) not in curve points = [(0, 1), (0, 0)] - curve = Segment(points) + curve = FactorySegment.bezier(points) assert (0, 0) in curve assert (0, 1) in curve assert (0, 0.5) in curve @@ -155,7 +93,7 @@ def test_line(self): assert (-1, 0) not in curve points = [(0, 0), (1, 1)] - curve = Segment(points) + curve = FactorySegment.bezier(points) assert (0, 0) in curve assert (1, 1) in curve assert (0.5, 0.5) in curve @@ -188,10 +126,12 @@ def test_begin(self): def test_middle(self): half = Fraction(1, 2) points = [(0, 0), (1, 0)] - curve = Segment(points) - curvea, curveb = curve.split([half]) - assert curvea == Segment([(0, 0), (half, 0)]) - assert curveb == Segment([(half, 0), (1, 0)]) + curve = FactorySegment.bezier(points) + curvea = FactorySegment.bezier([(0, 0), (half, 0)]) + curveb = FactorySegment.bezier([(half, 0), (1, 0)]) + assert curve.extract([0, half]) == curvea + assert curve.extract([half, 1]) == curveb + assert curve.split([half]) == (curvea, curveb) test = curvea | curveb assert test == curve @@ -216,7 +156,7 @@ def test_end(self): ] ) def test_print(): - segment = Segment([(0, 0), (1, 0), (0, 1)]) + segment = FactorySegment.bezier([(0, 0), (1, 0), (0, 1)]) str(segment) repr(segment) @@ -227,7 +167,6 @@ def test_print(): "test_begin", "test_build", "TestDerivate::test_end", - "TestOperations::test_end", "TestContains::test_end", "TestSplitUnite::test_end", "test_print", diff --git a/tests/geometry/test_usegment.py b/tests/geometry/test_usegment.py index e7c89b48..a8b7e6fb 100644 --- a/tests/geometry/test_usegment.py +++ b/tests/geometry/test_usegment.py @@ -5,7 +5,7 @@ import pytest from shapepy.geometry.box import Box -from shapepy.geometry.segment import Segment +from shapepy.geometry.factory import FactorySegment from shapepy.geometry.unparam import USegment from shapepy.scalar.angle import Angle @@ -25,7 +25,8 @@ def test_begin(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_build(): - segment = Segment([(0, 0), (1, 0), (0, 1)]) + points = [(0, 0), (1, 0), (0, 1)] + segment = FactorySegment.bezier(points) USegment(segment) @@ -33,7 +34,7 @@ def test_build(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_length(): - segment = Segment([(0, 0), (3, 4)]) + segment = FactorySegment.bezier([(0, 0), (3, 4)]) usegment = USegment(segment) assert usegment.length == 5 @@ -42,7 +43,7 @@ def test_length(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_box(): - segment = Segment([(0, 0), (3, 4)]) + segment = FactorySegment.bezier([(0, 0), (3, 4)]) usegment = USegment(segment) assert usegment.box() == Box((0, 0), (3, 4)) @@ -51,11 +52,11 @@ def test_box(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_move(): - segment = Segment([(0, 0), (3, 4)]) + segment = FactorySegment.bezier([(0, 0), (3, 4)]) usegment = USegment(segment) usegment.move((1, 2)) - good = Segment([(1, 2), (4, 6)]) + good = FactorySegment.bezier([(1, 2), (4, 6)]) assert usegment.parametrize() == good @@ -63,11 +64,11 @@ def test_move(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_scale(): - segment = Segment([(0, 0), (3, 4)]) + segment = FactorySegment.bezier([(0, 0), (3, 4)]) usegment = USegment(segment) usegment.scale(4) - good = Segment([(0, 0), (12, 16)]) + good = FactorySegment.bezier([(0, 0), (12, 16)]) assert usegment.parametrize() == good @@ -75,12 +76,12 @@ def test_scale(): @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["test_begin"]) def test_rotate(): - segment = Segment([(0, 0), (3, 4)]) + segment = FactorySegment.bezier([(0, 0), (3, 4)]) usegment = USegment(segment) angle = Angle.degrees(90) usegment.rotate(angle) - good = Segment([(0, 0), (-4, 3)]) + good = FactorySegment.bezier([(0, 0), (-4, 3)]) assert usegment.parametrize() == good