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 main(): doc = ezdxf.new() doc.layers.add(ENTITIES, color=colors.YELLOW) doc.layers.add(INTERSECTION_POINTS, color=colors.RED) doc.layers.add(CURVE_APPROXIMATIONS, color=colors.CYAN) msp = doc.modelspace() ellipse = msp.add_ellipse( center=(0, 0), major_axis=(3, 0), ratio=0.5, dxfattribs=GfxAttribs(layer=ENTITIES), ) fit_points = [(-4, -4), (-2, -1), (2, 1), (4, 4)] spline = msp.add_spline_control_frame( fit_points, dxfattribs=GfxAttribs(layer=ENTITIES) ) p1 = Vec2.list(ellipse.flattening(distance=0.001)) p2 = Vec2.list(spline.flattening(distance=0.001)) msp.add_lwpolyline(p1, dxfattribs=GfxAttribs(layer=CURVE_APPROXIMATIONS)) msp.add_lwpolyline(p2, dxfattribs=GfxAttribs(layer=CURVE_APPROXIMATIONS)) res = intersect_polylines_2d(p1, p2) for point in res: msp.add_circle( center=point, radius=0.1, dxfattribs=GfxAttribs(layer=INTERSECTION_POINTS), ) doc.set_modelspace_vport(height=10) doc.saveas(DIR / "intersect_ellipse_and_spline.dxf")
def test_coincident_common_segment(self): """ The common segment does not create intersection points. Same as intersection of coincident lines! """ pline1 = Vec2.list([(1, 1), (2, 1)]) pline2 = Vec2.list([(1, 1), (2, 1)]) res = intersect_polylines_2d(pline1, pline2) assert len(res) == 0
def test_intersecting_squares(self): square1 = forms.close_polygon(forms.square(2.0)) square2 = forms.translate(square1, (1, 1)) res = intersect_polylines_2d(Vec2.list(square1), Vec2.list(square2)) assert len(res) == 2 res.sort() assert res[0].isclose(Vec2(1, 2)) assert res[1].isclose(Vec2(2, 1))
def test_zig_zag_lines_with_common_vertices(self): pline1 = Vec2.list([(0, 0), (2, 2), (4, 0), (6, 2), (8, 0)]) pline2 = Vec2.list([(0, 4), (2, 2), (4, 4), (6, 2), (8, 4)]) res = intersect_polylines_2d(pline1, pline2) assert len(res) == 2 res.sort() # do not rely on any order assert res[0].isclose(Vec2(2, 2)) assert res[1].isclose(Vec2(6, 2))
def add_spline( self, fit_points: Iterable["Vertex"] = None, control_points: Iterable["Vertex"] = None, knot_values: Iterable[float] = None, weights: Iterable[float] = None, degree: int = 3, periodic: int = 0, start_tangent: "Vertex" = None, end_tangent: "Vertex" = None, ) -> "SplineEdge": """Add a :class:`SplineEdge`. Args: fit_points: points through which the spline must go, at least 3 fit points are required. list of (x, y)-tuples control_points: affects the shape of the spline, mandatory and AutoCAD crashes on invalid data. list of (x, y)-tuples knot_values: (knot vector) mandatory and AutoCAD crashes on invalid data. list of floats; `ezdxf` provides two tool functions to calculate valid knot values: :func:`ezdxf.math.uniform_knot_vector`, :func:`ezdxf.math.open_uniform_knot_vector` (default if ``None``) weights: weight of control point, not mandatory, list of floats. degree: degree of spline (int) periodic: 1 for periodic spline, 0 for none periodic spline start_tangent: start_tangent as 2d vector, optional end_tangent: end_tangent as 2d vector, optional .. warning:: Unlike for the spline entity AutoCAD does not calculate the necessary `knot_values` for the spline edge itself. On the contrary, if the `knot_values` in the spline edge are missing or invalid AutoCAD **crashes**. """ spline = SplineEdge() if fit_points is not None: spline.fit_points = Vec2.list(fit_points) if control_points is not None: spline.control_points = Vec2.list(control_points) if knot_values is not None: spline.knot_values = list(knot_values) else: spline.knot_values = list( open_uniform_knot_vector(len(spline.control_points), degree + 1) ) if weights is not None: spline.weights = list(weights) spline.degree = degree spline.rational = int(bool(len(spline.weights))) spline.periodic = int(periodic) if start_tangent is not None: spline.start_tangent = Vec2(start_tangent) if end_tangent is not None: spline.end_tangent = Vec2(end_tangent) self.edges.append(spline) return spline
def test_coincident_common_last_segment(self): """ The common segment does not create intersection points, but the preceding segment does. """ pline1 = Vec2.list([(0, 0), (1, 1), (2, 1)]) pline2 = Vec2.list([(0, 2), (1, 1), (2, 1)]) res = intersect_polylines_2d(pline1, pline2) assert len(res) == 1 assert res[0].isclose(Vec2(1, 1))
def test_intersecting_zig_zag_lines(self): pline1 = Vec2.list([(0, 0), (2, 2), (4, 0), (6, 2), (8, 0)]) pline2 = Vec2.list([(0, 2), (2, 0), (4, 2), (6, 0), (8, 2)]) res = intersect_polylines_2d(pline1, pline2) assert len(res) == 4 res.sort() # do not rely on any order assert res[0].isclose(Vec2(1, 1)) assert res[1].isclose(Vec2(3, 1)) assert res[2].isclose(Vec2(5, 1)) assert res[3].isclose(Vec2(7, 1))
def test_coincident_common_intermediate_segment(self): """ The common segment does not create intersection points, but the preceding and the following segment does. """ pline1 = Vec2.list([(0, 0), (1, 1), (2, 1), (3, 0)]) pline2 = Vec2.list([(0, 2), (1, 1), (2, 1), (3, 2)]) res = intersect_polylines_2d(pline1, pline2) assert len(res) == 2 res.sort() assert res[0].isclose(Vec2(1, 1)) assert res[1].isclose(Vec2(2, 1))
def test_curve4_to(self): p = path.Path() p.curve4_to((4, 0, 2), (1, 1, 7), (3, 1, 5)) mpath = path.to_matplotlib_path([p]) assert tuple(mpath.codes) == (MC.MOVETO, MC.CURVE4, MC.CURVE4, MC.CURVE4) assert Vec2.list(mpath.vertices) == [(0, 0), (1, 1), (3, 1), (4, 0)]
def offset_vertices_2d(vertices: Iterable['Vertex'], offset: float, closed: bool = False) -> Iterable['Vec2']: """ Yields vertices of the offset line to the shape defined by `vertices`. The source shape consist of straight segments and is located in the xy-plane, the z-axis of input vertices is ignored. Takes closed shapes into account if argument `closed` is ``True``, which yields intersection of first and last offset segment as first vertex for a closed shape. For closed shapes the first and last vertex can be equal, else an implicit closing segment from last to first vertex is added. A shape with equal first and last vertex is not handled automatically as closed shape. .. warning:: Adjacent collinear segments in `opposite` directions, same as a turn by 180 degree (U-turn), leads to unexpected results. Args: vertices: source shape defined by vertices offset: line offset perpendicular to direction of shape segments defined by vertices order, offset > ``0`` is 'left' of line segment, offset < ``0`` is 'right' of line segment closed: ``True`` to handle as closed shape """ vertices = Vec2.list(vertices) if len(vertices) < 2: raise ValueError('2 or more vertices required.') if closed and not vertices[0].isclose(vertices[-1]): # append first vertex as last vertex to close shape vertices.append(vertices[0]) # create offset segments offset_segments = list() for start, end in zip(vertices[:-1], vertices[1:]): offset_vec = (end - start).orthogonal().normalize(offset) offset_segments.append((start + offset_vec, end + offset_vec)) if closed: # insert last segment also as first segment offset_segments.insert(0, offset_segments[-1]) # first offset vertex = start point of first segment for open shapes if not closed: yield offset_segments[0][0] # yield intersection points of offset_segments if len(offset_segments) > 1: for (start1, end1), (start2, end2) in zip(offset_segments[:-1], offset_segments[1:]): try: # the usual case yield ConstructionRay(start1, end1).intersect( ConstructionRay(start2, end2)) except ParallelRaysError: # collinear segments yield end1 if not end1.isclose(start2): # it's an U-turn (180 deg) # creates an additional vertex! yield start2 # last offset vertex = end point of last segment for open shapes if not closed: yield offset_segments[-1][1]
def test_line_to(self): p = path.Path() p.line_to((4, 5, 6)) p.line_to((7, 8, 6)) mpath = path.to_matplotlib_path([p]) assert tuple(mpath.codes) == (MC.MOVETO, MC.LINETO, MC.LINETO) assert Vec2.list(mpath.vertices) == [(0, 0), (4, 5), (7, 8)]
def convex_hull_2d(points: Iterable["Vertex"]) -> List[Vec2]: """Returns 2D convex hull for `points` as list of :class:`Vec2`. Returns a closed polyline, first vertex == last vertex. Args: points: iterable of points, z-axis is ignored """ # Source: https://massivealgorithms.blogspot.com/2019/01/convex-hull-sweep-line.html?m=1 def cross(o: Vec2, a: Vec2, b: Vec2) -> float: return (a - o).det(b - o) vertices = Vec2.list(set(points)) vertices.sort() if len(vertices) < 3: raise ValueError( "Convex hull calculation requires 3 or more unique points.") n: int = len(vertices) hull: List[Vec2] = [Vec2()] * (2 * n) k: int = 0 i: int for i in range(n): while k >= 2 and cross(hull[k - 2], hull[k - 1], vertices[i]) <= 0.0: k -= 1 hull[k] = vertices[i] k += 1 t: int = k + 1 for i in range(n - 2, -1, -1): while k >= t and cross(hull[k - 2], hull[k - 1], vertices[i]) <= 0.0: k -= 1 hull[k] = vertices[i] k += 1 return hull[:k]
def test_two_paths(self): p1 = path.Path() p1.line_to((4, 5, 6)) p2 = path.Path() p2.line_to((7, 8, 6)) mpath = path.to_matplotlib_path([p1, p2]) assert tuple(mpath.codes) == ( MC.MOVETO, MC.LINETO, MC.MOVETO, MC.LINETO, ) assert Vec2.list(mpath.vertices) == [ (0, 0), (4, 5), (0, 0), (7, 8), ]
def set_boundary_path(self, vertices: Iterable["Vertex"]) -> None: """Set boundary path to `vertices`. Two vertices describe a rectangle (lower left and upper right corner), more than two vertices is a polygon as clipping path. """ _vertices = Vec2.list(vertices) if len(_vertices): if len(_vertices) > 2 and not _vertices[-1].isclose(_vertices[0]): # Close path, otherwise AutoCAD crashes _vertices.append(_vertices[0]) self._boundary_path = _vertices self.set_flag_state(self.USE_CLIPPING_BOUNDARY, state=True) self.dxf.clipping = 1 self.dxf.clipping_boundary_type = 1 if len(_vertices) < 3 else 2 self.dxf.count_boundary_points = len(self._boundary_path) else: self.reset_boundary_path()
def set_masking_area(self, vertices: Iterable["Vertex"]) -> None: """Set a new masking area, the area is placed in the layout xy-plane.""" self.update_dxf_attribs(self.DEFAULT_ATTRIBS) vertices = Vec2.list(vertices) bounds = BoundingBox2d(vertices) x_size, y_size = bounds.size dxf = self.dxf dxf.insert = Vec3(bounds.extmin) dxf.u_pixel = Vec3(x_size, 0, 0) dxf.v_pixel = Vec3(0, y_size, 0) def boundary_path(): extmin = bounds.extmin for vertex in vertices: v = vertex - extmin yield Vec2(v.x / x_size - 0.5, 0.5 - v.y / y_size) self.set_boundary_path(boundary_path())
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 __init__(self, vertices: Iterable['Vertex'] = None): self.vertices: List[Vec2] = [] if vertices is None else Vec2.list( vertices)
def test_subdivide_vec2_square_in_quads(): b = Vec2.list(square(2)) result = list(subdivide_face(b, quads=True)) assert len(result) == 4 assert result[0] == ((0, 0), (1, 0), (1, 1), (0, 1))
def polygon(vertices: Iterable["Vertex"]) -> List[Vec2]: _vertices = Vec2.list(vertices) if len(_vertices) > 1: if _vertices[0].isclose(_vertices[-1]): _vertices.pop() return _vertices
def points3(self): return Vec2.list([(0, 0), (0, 1), (1.5, 0.75), (2, 2)])
def test_inside_horiz_box(): square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)]) assert is_point_in_polygon_2d(Vec2(.5, .5), square) == 1
def test_circle_inside_rect(rect): c = Vec2.list(circle(16, 0.7)) result = clip_polygon_2d(rect, c, ccw_check=False) assert len(result) == 16 for v in c: assert v in result
def test_colinear_outside_horiz_box(): square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)]) assert is_point_in_polygon_2d(Vec2(1.5, 0), square) == -1 assert is_point_in_polygon_2d(Vec2(-.5, 0), square) == -1 assert is_point_in_polygon_2d(Vec2(0, 1.5), square) == -1 assert is_point_in_polygon_2d(Vec2(0, -.5), square) == -1
def test_borders_slanted_box_stable(): square = Vec2.list([(0, 0), (1, 1), (0, 2), (-1, 1)]) assert is_point_in_polygon_2d(Vec2(0.5, 0.5), square) == 0 assert is_point_in_polygon_2d(Vec2(0.5, 1.5), square) == 0 assert is_point_in_polygon_2d(Vec2(-.5, 1.5), square) == 0 assert is_point_in_polygon_2d(Vec2(-.5, 0.5), square) == 0
def __init__(self, data): self.cities = Vec2.list(data)
def test_corners_horiz_box(): square = Vec2.list([(0, 0), (1, 0), (1, 1), (0, 1)]) assert is_point_in_polygon_2d(Vec2(0, 0), square) == 0 assert is_point_in_polygon_2d(Vec2(0, 1), square) == 0 assert is_point_in_polygon_2d(Vec2(1, 1), square) == 0 assert is_point_in_polygon_2d(Vec2(0, 1), square) == 0
def test_outside_slanted_box(): square = Vec2.list([(0, 0), (1, 1), (0, 2), (-1, 1)]) assert is_point_in_polygon_2d(Vec2(-1, 0), square) == -1 assert is_point_in_polygon_2d(Vec2(1, 0), square) == -1 assert is_point_in_polygon_2d(Vec2(1, 2), square) == -1 assert is_point_in_polygon_2d(Vec2(-1, 2), square) == -1
def export_dxf(self, tagwriter: "TagWriter") -> None: def set_required_tangents(points: List[Vec2]): if len(points) > 1: if self.start_tangent is None: self.start_tangent = points[1] - points[0] if self.end_tangent is None: self.end_tangent = points[-1] - points[-2] if len(self.weights): if len(self.weights) == len(self.control_points): self.rational = 1 else: raise const.DXFValueError( "SplineEdge: count of control points and count of weights " "mismatch" ) else: self.rational = 0 write_tag = tagwriter.write_tag2 write_tag(72, 4) # edge type write_tag(94, int(self.degree)) write_tag(73, int(self.rational)) write_tag(74, int(self.periodic)) write_tag(95, len(self.knot_values)) # number of knots write_tag(96, len(self.control_points)) # number of control points # build knot values list # knot values have to be present and valid, otherwise AutoCAD crashes if len(self.knot_values): for value in self.knot_values: write_tag(40, float(value)) else: raise const.DXFValueError( "SplineEdge: missing required knot values" ) # build control points # control points have to be present and valid, otherwise AutoCAD crashes cp = Vec2.list(self.control_points) if self.rational: for point, weight in zip(cp, self.weights): write_tag(10, float(point.x)) write_tag(20, float(point.y)) write_tag(42, float(weight)) else: for x, y in cp: write_tag(10, float(x)) write_tag(20, float(y)) # build optional fit points if len(self.fit_points) > 0: set_required_tangents(cp) write_tag(97, len(self.fit_points)) for x, y, *_ in self.fit_points: write_tag(11, float(x)) write_tag(21, float(y)) elif tagwriter.dxfversion >= const.DXF2010: # (97, 0) len tag required by AutoCAD 2010+ write_tag(97, 0) if self.start_tangent is not None: x, y, *_ = self.start_tangent write_tag(12, float(x)) write_tag(22, float(y)) if self.end_tangent is not None: x, y, *_ = self.end_tangent write_tag(13, float(x)) write_tag(23, float(y))
def test_corners_slanted_box(): square = Vec2.list([(0, 0), (1, 1), (0, 2), (-1, 1)]) assert is_point_in_polygon_2d(Vec2(0, 0), square) == 0 assert is_point_in_polygon_2d(Vec2(1, 1), square) == 0 assert is_point_in_polygon_2d(Vec2(0, 2), square) == 0 assert is_point_in_polygon_2d(Vec2(-1, 1), square) == 0