def test_complex_ellipse_with_spline_intersection(self): ellipse = ConstructionEllipse(center=(0, 0), major_axis=(3, 0), ratio=0.5) bspline = BSpline([(-4, -4), (-2, -1), (2, 1), (4, 4)]) p1 = ellipse.flattening(distance=0.01) p2 = bspline.flattening(distance=0.01) res = intersect_polylines_2d(Vec2.list(p1), Vec2.list(p2)) assert len(res) == 2
def test_rational_spline_from_elliptic_arc_has_expected_parameters(): ellipse = ConstructionEllipse( center=(1, 1), major_axis=(2, 0), ratio=0.5, start_param=0, end_param=math.pi / 2, ) spline = rational_bspline_from_ellipse(ellipse) assert spline.degree == 2 cpoints = spline.control_points assert len(cpoints) == 3 assert cpoints[0].isclose((3, 1, 0)) assert cpoints[1].isclose((3, 2, 0)) assert cpoints[2].isclose((1, 2, 0)) weights = spline.weights() assert len(weights) == 3 assert weights[0] == 1.0 assert weights[1] == math.cos(math.pi / 4) assert weights[2] == 1.0 # as BSpline constructor() s2 = BSpline.from_ellipse(ellipse) assert spline.control_points == s2.control_points
def test_add_ellipse(): from ezdxf.math import ConstructionEllipse ellipse = ConstructionEllipse(center=(3, 0), major_axis=(1, 0), ratio=0.5, start_param=0, end_param=math.pi) path = Path() tools.add_ellipse(path, ellipse) assert path.start == (4, 0) assert path.end == (2, 0) # set start point to end of ellipse path = Path(start=(2, 0)) # add reversed ellipse, by default the start of # an empty path is set to the ellipse start tools.add_ellipse(path, ellipse, reset=False) assert path.start == (2, 0) assert path.end == (4, 0) path = Path() # add a line segment from (0, 0) to start of ellipse tools.add_ellipse(path, ellipse, reset=False) assert path.start == (0, 0) assert path.end == (2, 0)
def test_params_from_vertices_random(): center = Vector.random(5) major_axis = Vector.random(5) extrusion = Vector.random() ratio = 0.75 e = ConstructionEllipse(center, major_axis, extrusion, ratio) params = [random.uniform(0.0001, math.tau - 0.0001) for _ in range(20)] vertices = e.vertices(params) new_params = e.params_from_vertices(vertices) for expected, param in zip(params, new_params): assert math.isclose(expected, param) # This creates the same vertex as v1 and v2 v1, v2 = e.vertices([0, math.tau]) assert v1.isclose(v2) # This should create the same param for v1 and v2, but # floating point inaccuracy produces unpredictable results: p1, p2 = e.params_from_vertices((v1, v2)) assert math.isclose(p1, 0, abs_tol=1e-9) or math.isclose( p1, math.tau, abs_tol=1e-9) assert math.isclose(p2, 0, abs_tol=1e-9) or math.isclose( p2, math.tau, abs_tol=1e-9)
def test_default_init(): e = ConstructionEllipse() assert e.center == (0, 0, 0) assert e.major_axis == (1, 0, 0) assert e.minor_axis == (0, 1, 0) assert e.extrusion == (0, 0, 1) assert e.ratio == 1.0 assert e.start_param == 0 assert e.end_param == math.tau
def bezier4p_from_ellipse(func, count): ellipse = ConstructionEllipse( center=(1, 2), major_axis=(2, 0), ratio=0.5, start_param=0, end_param=math.tau, ) for _ in range(count): list(func(ellipse))
def test_from_ellipse(): from ezdxf.entities import Ellipse from ezdxf.math import ConstructionEllipse e = ConstructionEllipse(center=(3, 0), major_axis=(1, 0), ratio=0.5, start_param=0, end_param=math.pi) ellipse = Ellipse.new() ellipse.apply_construction_tool(e) path = Path.from_ellipse(ellipse) assert path.start == (4, 0) assert path.end == (2, 0)
def test_dxfattribs(): e = ConstructionEllipse() attribs = e.dxfattribs() assert attribs["center"] == (0, 0, 0) assert attribs["major_axis"] == (1, 0, 0) assert "minor_axis" not in attribs assert attribs["extrusion"] == (0, 0, 1) assert attribs["ratio"] == 1.0 assert attribs["start_param"] == 0 assert attribs["end_param"] == math.tau
def test_dxfattribs(): e = ConstructionEllipse() attribs = e.dxfattribs() assert attribs['center'] == (0, 0, 0) assert attribs['major_axis'] == (1, 0, 0) assert 'minor_axis' not in attribs assert attribs['extrusion'] == (0, 0, 1) assert attribs['ratio'] == 1.0 assert attribs['start_param'] == 0 assert attribs['end_param'] == math.tau
def construction_tool(self) -> ConstructionEllipse: """Returns construction tool :class:`ezdxf.math.ConstructionEllipse`.""" dxf = self.dxf return ConstructionEllipse( dxf.center, dxf.major_axis, dxf.extrusion, dxf.ratio, dxf.start_param, dxf.end_param, )
def add_ellipse_edge(edge): ocs_ellipse = edge.construction_tool() # ConstructionEllipse has WCS representation: ellipse = ConstructionEllipse( center=wcs(ocs_ellipse.center.replace(z=elevation)), major_axis=wcs(ocs_ellipse.major_axis), ratio=ocs_ellipse.ratio, extrusion=extrusion, start_param=ocs_ellipse.start_param, end_param=ocs_ellipse.end_param, ) tools.add_ellipse(path, ellipse, reset=not bool(path))
def construction_tool(self) -> ConstructionEllipse: """Returns ConstructionEllipse() for the OCS representation.""" return ConstructionEllipse( center=Vec3(self.center), major_axis=Vec3(self.major_axis), extrusion=Vec3(0, 0, 1), ratio=self.ratio, # 1. ConstructionEllipse() is always in ccw orientation # 2. Start- and end params are always stored in ccw orientation start_param=self.start_param, end_param=self.end_param, )
def test_rational_spline_from_complex_elliptic_arc(): ellipse = ConstructionEllipse( center=(49.64089977339618, 36.43095770602131, 0.0), major_axis=(16.69099826506408, 6.96203799241026, 0.0), ratio=0.173450304570581, start_param=5.427509144462117, end_param=7.927025930557775, ) curves = list(cubic_bezier_from_ellipse(ellipse)) assert curves[0].control_points[0].isclose(ellipse.start_point) assert curves[1].control_points[-1].isclose(ellipse.end_point)
def test_rational_spline_from_elliptic_arc_has_same_end_points(): ellipse = ConstructionEllipse( center=(1, 1), major_axis=(2, 0), ratio=0.5, start_param=math.radians(30), end_param=math.radians(330), ) start_point = ellipse.start_point end_point = ellipse.end_point spline = rational_bspline_from_ellipse(ellipse) assert start_point.isclose(spline.control_points[0]) assert end_point.isclose(spline.control_points[-1])
def test_swap_axis_full_ellipse(): ellipse = ConstructionEllipse( major_axis=(5, 0, 0), ratio=2, ) assert ellipse.minor_axis.isclose((0, 10, 0)) ellipse.swap_axis() assert ellipse.ratio == 0.5 assert ellipse.major_axis == (0, 10, 0) assert ellipse.minor_axis == (-5, 0, 0) assert ellipse.start_param == 0 assert ellipse.end_param == math.pi * 2
def test_tangents(): e = ConstructionEllipse(center=(3, 3), major_axis=(2, 0), ratio=0.5, start_param=0, end_param=math.pi * 1.5) params = list(e.params(7)) result = [(0.0, 1.0, 0.0), (-0.894427190999916, 0.447213595499958, 0.0), (-1.0, 3.061616997868383e-17, 0.0), (-0.894427190999916, -0.4472135954999579, 0.0), (-2.4492935982947064e-16, -1.0, 0.0), (0.8944271909999159, -0.44721359549995804, 0.0), (1.0, 0.0, 0.0)] for v, r in zip(e.tangents(params), result): assert v.isclose(r)
def test_rational_spline_from_elliptic_arc(): ellipse = ConstructionEllipse( center=(1, 1), major_axis=(2, 0), ratio=0.5, start_param=0, end_param=math.pi / 2, ) curves = list(cubic_bezier_from_ellipse(ellipse)) assert len(curves) == 1 p1, p2, p3, p4 = curves[0].control_points assert p1.isclose((3, 1, 0)) assert p2.isclose((3.0, 1.5522847498307932, 0)) assert p3.isclose((2.104569499661587, 2.0, 0)) assert p4.isclose((1, 2, 0))
def test_vertices(): e = ConstructionEllipse(center=(3, 3), major_axis=(2, 0), ratio=0.5, start_param=0, end_param=math.pi * 1.5) params = list(e.params(7)) result = [ (5.0, 3.0, 0.0), (4.414213562373095, 3.7071067811865475, 0.0), (3.0, 4.0, 0.0), (1.585786437626905, 3.7071067811865475, 0.0), (1.0, 3.0, 0.0), (1.5857864376269046, 2.2928932188134525, 0.0), (3.0, 2.0, 0.0), ] for v, r in zip(e.vertices(params), result): assert v.isclose(r) v1, v2 = e.vertices([0, math.tau]) assert v1 == v2
def ellipse(edge: EllipseEdge): ocs_ellipse = edge.construction_tool() # ConstructionEllipse has WCS representation: # Note: clockwise oriented ellipses are converted to counter # clockwise ellipses at the loading stage! # See: ezdxf.entities.boundary_paths.EllipseEdge.load_tags() ellipse = ConstructionEllipse( center=wcs(ocs_ellipse.center.replace(z=elevation)), major_axis=wcs_tangent(ocs_ellipse.major_axis), ratio=ocs_ellipse.ratio, extrusion=extrusion, start_param=ocs_ellipse.start_param, end_param=ocs_ellipse.end_param, ) segment = Path() tools.add_ellipse(segment, ellipse, reset=True) return segment
def test_angle_to_param(): random_tests_count = 100 random.seed(0) angle = 1.23 assert math.isclose(angle_to_param(1.0, angle), angle) angle = 1.23 + math.pi / 2 assert math.isclose(angle_to_param(1.0, angle), angle) angle = 1.23 + math.pi assert math.isclose(angle_to_param(1.0, angle), angle) angle = 1.23 + 3 * math.pi / 2 assert math.isclose(angle_to_param(1.0, angle), angle) angle = math.pi / 2 + 1e-15 assert math.isclose(angle_to_param(1.0, angle), angle) for _ in range(random_tests_count): ratio = random.uniform(1e-6, 1) angle = random.uniform(0, math.tau) param = angle_to_param(ratio, angle) ellipse = ConstructionEllipse( # avoid (0, 0, 0) as major axis major_axis=(non_zero_random(), non_zero_random(), 0), ratio=ratio, start_param=0, end_param=param, extrusion=(0, 0, random.choice((1, -1))), ) calculated_angle = ellipse.extrusion.angle_about( ellipse.major_axis, ellipse.end_point) calculated_angle_without_direction = ellipse.major_axis.angle_between( ellipse.end_point) assert math.isclose(calculated_angle, angle, abs_tol=1e-9) assert math.isclose( calculated_angle, calculated_angle_without_direction) or math.isclose( math.tau - calculated_angle, calculated_angle_without_direction)
def test_get_start_and_end_vertex(): ellipse = ConstructionEllipse( center=(1, 2, 3), major_axis=(4, 3, 0), extrusion=(0, 0, -1), ratio=.7, start_param=math.pi / 2, end_param=math.pi, ) start, end = list(ellipse.vertices([ ellipse.start_param, ellipse.end_param, ])) # test values from BricsCAD assert start.isclose(Vec3(3.1, -0.8, 3), abs_tol=1e-6) assert end.isclose(Vec3(-3, -1, 3), abs_tol=1e-6) # for convenience, but vertices() is much more efficient: assert ellipse.start_point.isclose(Vec3(3.1, -0.8, 3), abs_tol=1e-6) assert ellipse.end_point.isclose(Vec3(-3, -1, 3), abs_tol=1e-6)
def to_spline_edge(e: EllipseEdge) -> SplineEdge: # No OCS transformation needed, source ellipse and target spline # reside in the same OCS. ellipse = ConstructionEllipse( center=e.center, major_axis=e.major_axis, ratio=e.ratio, start_param=e.start_param, end_param=e.end_param, ) count = max(int(float(num) * ellipse.param_span / math.tau), 3) tool = BSpline.ellipse_approximation(ellipse, count) spline = SplineEdge() spline.degree = tool.degree if not e.ccw: tool = tool.reverse() spline.control_points = Vec2.list(tool.control_points) spline.knot_values = tool.knots() # type: ignore spline.weights = tool.weights() # type: ignore return spline
def test_bezier_curves_ellipse_issue_708(ellipse): ellipse_ = ConstructionEllipse( center=(1.5, 0.375, 0.0), major_axis=(0.625, 0.0, 0.0), ratio=1.0, start_param=-2.498091544796509, end_param=-0.6435011087932844, ) curves = list(ellipse(ellipse_)) assert curves[0].control_points == (Vec3(0.9999999999999999, 1.6653345369377348e-16, 0.0), Vec3(1.1180339887498947, -0.15737865166652631, 0.0), Vec3(1.3032766854168418, -0.2499999999999999, 0.0), Vec3(1.4999999999999998, -0.25, 0.0)) assert curves[1].control_points == (Vec3(1.4999999999999998, -0.25, 0.0), Vec3(1.696723314583158, -0.25, 0.0), Vec3(1.881966011250105, -0.15737865166652654, 0.0), Vec3(2.0, -5.551115123125783e-17, 0.0))
def test_swap_axis_arbitrary_params(): random_tests_count = 100 random.seed(0) for _ in range(random_tests_count): ellipse = ConstructionEllipse( # avoid (0, 0, 0) as major axis major_axis=(non_zero_random(), non_zero_random(), 0), ratio=2, start_param=random.uniform(0, math.tau), end_param=random.uniform(0, math.tau), extrusion=(0, 0, random.choice((1, -1))), ) # Test if coordinates of start- and end point stay at the same location # before and after swapping axis. start_point = ellipse.start_point end_point = ellipse.end_point minor_axis = ellipse.minor_axis ellipse.swap_axis() assert ellipse.major_axis.isclose(minor_axis, abs_tol=1e-9) assert ellipse.start_point.isclose(start_point, abs_tol=1e-9) assert ellipse.end_point.isclose(end_point, abs_tol=1e-9)
def to_line_edges(edge): # Start- and end params are always stored in counter clockwise order! ellipse = ConstructionEllipse( center=edge.center, major_axis=edge.major_axis, ratio=edge.ratio, start_param=edge.start_param, end_param=edge.end_param, ) segment_count = max( int(float(num) * ellipse.param_span / math.tau), 3 ) params = ellipse.params(segment_count + 1) # Reverse path if necessary! if not edge.ccw: params = reversed(list(params)) vertices = list(ellipse.vertices(params)) for v1, v2 in zip(vertices[:-1], vertices[1:]): line = LineEdge() line.start = v1.vec2 line.end = v2.vec2 yield line
def test_to_ocs(): e = ConstructionEllipse().to_ocs() assert e.center == (0, 0)
def test_params(): count = 9 e = ConstructionEllipse(start_param=math.pi / 2, end_param=-math.pi / 2) params = list(e.params(count)) expected = list(linspace(math.pi / 2, math.pi / 2.0 * 3.0, count)) assert params == expected
def test_no_ellipse(self, start, end): e = ConstructionEllipse(start_param=start, end_param=end) assert e.param_span == 0.0
def test_full_ellipse(self, start, end): e = ConstructionEllipse(start_param=start, end_param=end) assert e.param_span == pytest.approx(math.tau)
def test_elliptic_arc(self, start, end, expected): e = ConstructionEllipse(start_param=start, end_param=end) assert e.param_span == pytest.approx(expected)