def parse(geo_mapping: Dict) -> Dict: """Parse ``__geo_interface__`` convert all coordinates into :class:`Vec3` objects, Polygon['coordinates'] is always a tuple (exterior, holes), holes maybe an empty list. """ geo_mapping = copy.deepcopy(geo_mapping) type_ = geo_mapping.get(TYPE) if type_ is None: raise ValueError(f'Required key "{TYPE}" not found.') if type_ == FEATURE_COLLECTION: # It is possible for this array to be empty. features = geo_mapping.get(FEATURES) if features: geo_mapping[FEATURES] = [parse(f) for f in features] else: raise ValueError(f'Missing key "{FEATURES}" in FeatureCollection.') elif type_ == GEOMETRY_COLLECTION: # It is possible for this array to be empty. geometries = geo_mapping.get(GEOMETRIES) if geometries: geo_mapping[GEOMETRIES] = [parse(g) for g in geometries] else: raise ValueError( f'Missing key "{GEOMETRIES}" in GeometryCollection.') elif type_ == FEATURE: # The value of the geometry member SHALL be either a Geometry object # or, in the case that the Feature is unlocated, a JSON null value. if GEOMETRY in geo_mapping: geometry = geo_mapping.get(GEOMETRY) geo_mapping[GEOMETRY] = parse(geometry) if geometry else None else: raise ValueError(f'Missing key "{GEOMETRY}" in Feature.') elif type_ in { POINT, LINE_STRING, POLYGON, MULTI_POINT, MULTI_LINE_STRING, MULTI_POLYGON, }: coordinates = geo_mapping.get(COORDINATES) if coordinates is None: raise ValueError(f'Missing key "{COORDINATES}" in {type_}.') if type_ == POINT: coordinates = Vec3(coordinates) elif type_ in (LINE_STRING, MULTI_POINT): coordinates = Vec3.list(coordinates) elif type_ == POLYGON: coordinates = _parse_polygon(coordinates) elif type_ == MULTI_LINE_STRING: coordinates = [Vec3.list(v) for v in coordinates] elif type_ == MULTI_POLYGON: coordinates = [_parse_polygon(v) for v in coordinates] geo_mapping[COORDINATES] = coordinates else: raise TypeError(f'Invalid type "{type_}".') return geo_mapping
def _parse_polygon(coordinates: Sequence) -> Sequence: """ Returns polygon definition as tuple (exterior, [holes]). """ if _is_coordinate_sequence(coordinates): exterior = coordinates holes = [] else: exterior = coordinates[0] holes = coordinates[1:] return Vec3.list(exterior), [Vec3.list(h) for h in holes]
def test_polygon_with_holes_to_dxf_entity(): res = cast(Hatch, list(geo.dxf_entities(POLYGON_2))[0]) assert len(res.paths) == 3 p = res.paths[1] assert p.PATH_TYPE == 'PolylinePath' assert p.vertices == Vec3.list(HOLE1) p = res.paths[2] assert p.PATH_TYPE == 'PolylinePath' assert p.vertices == Vec3.list(HOLE2)
def test_polygon_with_holes_to_dxf_polygon(dxftype, polygon): entity = cast(DXFPolygon, list(geo.dxf_entities(POLYGON_2, polygon=polygon))[0]) assert entity.dxftype() == dxftype assert len(entity.paths) == 3 p = entity.paths[1] assert p.type == BoundaryPathType.POLYLINE assert p.vertices == Vec3.list(HOLE1) p = entity.paths[2] assert p.type == BoundaryPathType.POLYLINE assert p.vertices == Vec3.list(HOLE2)
def spline_insert_knot(): doc = ezdxf.new("R2000", setup=True) msp = doc.modelspace() def add_spline(control_points, color=3, knots=None): msp.add_polyline2d(control_points, dxfattribs={ "color": color, "linetype": "DASHED" }) msp.add_open_spline(control_points, degree=3, knots=knots, dxfattribs={"color": color}) control_points = Vec3.list([ (0, 0), (10, 20), (30, 10), (40, 10), (50, 0), (60, 20), (70, 50), (80, 70), ]) add_spline(control_points, color=3, knots=None) bspline = BSpline(control_points, order=4) bspline.insert_knot(bspline.max_t / 2) add_spline(bspline.control_points, color=4, knots=bspline.knots()) doc.saveas("Spline_R2000_spline_insert_knot.dxf")
def test_control_vertices(p1): vertices = list(p1.control_vertices()) assert vertices == Vec3.list([(0, 0), (2, 0), (2, 1), (4, 1), (4, 0)]) path = Path() assert len(list(path.control_vertices())) == 0 path = Path.from_vertices([(0, 0), (1, 0)]) assert len(list(path.control_vertices())) == 2
def best_fit_normal(vertices: Iterable['Vertex']) -> Vec3: """ Returns the "best fit" normal for a plane defined by three or more vertices. This function tolerates imperfect plane vertices. Safe function to detect the extrusion vector of flat arbitrary polygons. """ # Source: https://gamemath.com/book/geomprims.html#plane_best_fit (9.5.3) vertices = Vec3.list(vertices) if len(vertices) < 3: raise ValueError("3 or more vertices required") first = vertices[0] if not first.isclose(vertices[-1]): vertices.append(first) # close polygon prev_x, prev_y, prev_z = first.xyz nx = 0.0 ny = 0.0 nz = 0.0 for v in vertices[1:]: x, y, z = v.xyz nx += (prev_z + z) * (prev_y - y) ny += (prev_x + x) * (prev_z - z) nz += (prev_y + y) * (prev_x - x) prev_x = x prev_y = y prev_z = z return Vec3(nx, ny, nz).normalize()
def add_mesh(self, vertices: List[Vec3] = None, faces: List[Sequence[int]] = None, edges: List[Tuple[int, int]] = None, mesh=None) -> None: """ Add another mesh to this mesh. A `mesh` can be a :class:`MeshBuilder`, :class:`MeshVertexMerger` or :class:`~ezdxf.entities.Mesh` object or requires the attributes :attr:`vertices`, :attr:`edges` and :attr:`faces`. Args: vertices: list of vertices, a vertex is a ``(x, y, z)`` tuple or :class:`~ezdxf.math.Vec3` object faces: list of faces, a face is a list of vertex indices edges: list of edges, an edge is a list of vertex indices mesh: another mesh entity """ if mesh is not None: vertices = Vec3.list(mesh.vertices) faces = mesh.faces edges = mesh.edges if vertices is None: raise ValueError("Requires vertices or another mesh.") faces = faces or [] edges = edges or [] indices = self.add_vertices(vertices) for v1, v2 in edges: self.edges.append((indices[v1], indices[v2])) for face_vertices in faces: self.faces.append(tuple(indices[vi] for vi in face_vertices))
def test_points_from_wcs(): points = Vec3.list([(1, 2, 3), (3, 4, 5)]) ucs = PassTroughUCS() assert list(ucs.points_from_wcs(points)) == points ucs2 = UCS() assert list(ucs.points_from_wcs(points)) == list(ucs2.points_from_wcs(points))
def test_local_cubic_bspline_interpolation_from_tangents(): points = Vec3.list(POINTS1) tangents = estimate_tangents(points) control_points, knots = local_cubic_bspline_interpolation_from_tangents( points, tangents) assert len(control_points) == 8 assert len(knots) == 8 + 4 # count + order
def test_polygon_without_holes_to_dxf_entity(): res = cast(Hatch, list(geo.dxf_entities(POLYGON_0))[0]) assert res.dxftype() == 'HATCH' assert len(res.paths) == 1 p = res.paths[0] assert p.PATH_TYPE == 'PolylinePath' assert p.vertices == Vec3.list(EXTERIOR)
def test_polygon_without_holes_to_dxf_polygon(dxftype, polygon): entity = cast(DXFPolygon, list(geo.dxf_entities(POLYGON_0, polygon=polygon))[0]) assert entity.dxftype() == dxftype assert len(entity.paths) == 1 p = entity.paths[0] assert p.type == BoundaryPathType.POLYLINE assert p.vertices == Vec3.list(EXTERIOR)
def extrude( profile: Iterable["Vertex"], path: Iterable["Vertex"], close=True ) -> MeshTransformer: """Extrude a `profile` polygon along a `path` polyline, vertices of profile should be in counter clockwise order. Args: profile: sweeping profile as list of (x, y, z) tuples in counter clockwise order path: extrusion path as list of (x, y, z) tuples close: close profile polygon if ``True`` Returns: :class:`~ezdxf.render.MeshTransformer` """ def add_hull(bottom_profile, top_profile): prev_bottom = bottom_profile[0] prev_top = top_profile[0] for bottom, top in zip(bottom_profile[1:], top_profile[1:]): face = ( prev_bottom, bottom, top, prev_top, ) # counter clock wise: normals outwards mesh.faces.append(face) prev_bottom = bottom prev_top = top mesh = MeshVertexMerger() profile = Vec3.list(profile) if close: profile = close_polygon(profile) path = Vec3.list(path) start_point = path[0] # type: ignore bottom_indices = mesh.add_vertices(profile) # base profile for target_point in path[1:]: # type: ignore translation_vector = target_point - start_point # profile will just be translated profile = [vec + translation_vector for vec in profile] top_indices = mesh.add_vertices(profile) add_hull(bottom_indices, top_indices) bottom_indices = top_indices start_point = target_point return MeshTransformer.from_builder(mesh)
def test_control_vertices(p1): vertices = list(p1.control_vertices()) assert vertices == Vec3.list([(0, 0), (2, 0), (2, 1), (4, 1), (4, 0), (5, -1), (6, 0)]) path = Path() assert len(list(path.control_vertices())) == 0 assert list(path.control_vertices()) == list(path.approximate(2)) path = converter.from_vertices([(0, 0), (1, 0)]) assert len(list(path.control_vertices())) == 2
def test_knot_generation(p, method): fit_points = Vec3.list([(0, 0), (0, 10), (10, 10), (20, 10), (20, 0), (30, 0), (30, 10), (40, 10), (40, 0)]) count = len(fit_points) n = count - 1 order = p + 1 t_vector = distance_t_vector(fit_points) knots = list(knots_from_parametrization(n, p, t_vector, method)) check_knots(n + 1, p + 1, knots)
def cubic_bezier_interpolation( points: Iterable['Vertex']) -> Iterable[Bezier4P]: """ Returns an interpolation curve for given data `points` as multiple cubic Bézier-curves. Returns n-1 cubic Bézier-curves for n given data points, curve i goes from point[i] to point[i+1]. Args: points: data points .. versionadded:: 0.13 """ # Source: https://towardsdatascience.com/b%C3%A9zier-interpolation-8033e9a262c2 points = Vec3.list(points) if len(points) < 3: raise ValueError('At least 3 points required.') num = len(points) - 1 # setup tri-diagonal matrix (a, b, c) b = [4.0] * num a = [1.0] * num c = [1.0] * num b[0] = 2.0 b[num - 1] = 7.0 a[num - 1] = 2.0 # setup right-hand side quantities points_vector = [points[0] + 2.0 * points[1]] points_vector.extend(2.0 * (2.0 * points[i] + points[i + 1]) for i in range(1, num - 1)) points_vector.append(8.0 * points[num - 1] + points[num]) # solve tri-diagonal linear equation system solution = tridiagonal_matrix_solver((a, b, c), points_vector) control_points_1 = Vec3.list(solution.rows()) control_points_2 = [ p * 2.0 - cp for p, cp in zip(points[1:], control_points_1[1:]) ] control_points_2.append((control_points_1[num - 1] + points[num]) / 2.0) for defpoints in zip(points, control_points_1, control_points_2, points[1:]): yield Bezier4P(defpoints)
def ngon_to_triangles(face: Iterable["Vertex"]) -> Iterable[Sequence[Vec3]]: _face = Vec3.list(face) if _face[0].isclose(_face[-1]): # closed shape center = Vec3.sum(_face[:-1]) / (len(_face) - 1) else: center = Vec3.sum(_face) / len(_face) _face.append(_face[0]) for v1, v2 in zip(_face[:-1], _face[1:]): yield v1, v2, center
def from_vertices(cls, vertices: Iterable['Vertex'], close=False) -> 'Path': """ Returns a :class:`Path` from vertices. """ vertices = Vec3.list(vertices) if len(vertices) < 2: return cls() path = cls(start=vertices[0]) for vertex in vertices[1:]: path.line_to(vertex) if close: path.close() return path
def from_vertices(vertices: Iterable['Vertex'], close=False) -> Path: """ Returns a :class:`Path` object from the given `vertices`. """ vertices = Vec3.list(vertices) if len(vertices) < 2: return Path() path = Path(start=vertices[0]) for vertex in vertices[1:]: path.line_to(vertex) if close: path.close() return path
def from_vertices(vertices: Iterable["Vertex"], close=False) -> Path: """Returns a :class:`Path` object from the given `vertices`.""" _vertices = Vec3.list(vertices) if len(_vertices) < 2: return Path() path = Path(start=_vertices[0]) for vertex in _vertices[1:]: if not path.end.isclose(vertex): path.line_to(vertex) if close: path.close() return path
def fit_points_2(): return Vec3.list([ (0, 0), (0, 10), (10, 10), (20, 10), (20, 0), (30, 0), (30, 10), (40, 10), (40, 0), ])
def __init__(self, points: Iterable["Vertex"] = None, segments: int = 100): """ Args: points: spline definition points segments: count of line segments for approximation, vertex count is `segments` + 1 """ if points is None: points = [] self.points: List[Vec3] = Vec3.list(points) self.segments = int(segments)
def test_arc_distances(): p = Vec3.list([(0, 0), (2, 2), (4, 0), (6, -2), (8, 0)]) # p[1]..p[3] are a straight line, radius calculation fails and # a straight line from p[1] to p[2] is used as replacement # for the second arc radius = 2.0 arc_length = math.pi * 0.5 * radius diagonal = math.sqrt(2.0) * radius distances = list(arc_distances(p)) assert len(distances) == 4 assert isclose(distances[0], arc_length) assert isclose(distances[1], diagonal) # replacement for arc assert isclose(distances[2], arc_length) assert isclose(distances[3], arc_length)
def __init__( self, vertices: Iterable[Vertex], close: bool = False, rel_tol: float = REL_TOL, ): self._rel_tol = float(rel_tol) v3list: List[Vec3] = Vec3.list(vertices) self._vertices: List[Vec3] = v3list if close and len(v3list) > 2: if not v3list[0].isclose(v3list[-1], rel_tol=self._rel_tol): v3list.append(v3list[0]) self._distances: List[float] = _distances(v3list)
def area(vertices: Iterable['Vertex']) -> float: """ Returns the area of a polygon, returns the projected area in the xy-plane for 3D vertices. """ vertices = Vec3.list(vertices) if len(vertices) < 3: raise ValueError('At least 3 vertices required.') # Close polygon: if not vertices[0].isclose(vertices[-1]): vertices.append(vertices[0]) return abs(sum( (p1.x * p2.y - p1.y * p2.x) for p1, p2 in zip(vertices, vertices[1:]) ) / 2)
def __init__( self, control_points: Iterable["Vertex"], degree: int = 2, closed: bool = True, ): """ Args: control_points: B-spline control frame vertices degree: degree of B-spline, only 2 and 3 is supported closed: ``True`` for closed curve """ self.control_points = Vec3.list(control_points) self.degree = degree self.closed = closed
def extend(self, vertices: Iterable["Vertex"]) -> None: """Append multiple vertices to the reference line. It is possible to work with 3D vertices, but all vertices have to be in the same plane and the normal vector of this plan is stored as extrusion vector in the MLINE entity. """ vertices = Vec3.list(vertices) if not vertices: return all_vertices = [] if len(self): all_vertices.extend(self.get_locations()) all_vertices.extend(vertices) self.generate_geometry(all_vertices)
def has_clockwise_orientation(vertices: Iterable['Vertex']) -> bool: """ Returns True if 2D `vertices` have clockwise orientation. Ignores z-axis of all vertices. Args: vertices: iterable of :class:`Vec2` compatible objects Raises: ValueError: less than 3 vertices """ vertices = Vec3.list(vertices) if len(vertices) < 3: raise ValueError('At least 3 vertices required.') # Close polygon: if not vertices[0].isclose(vertices[-1]): vertices.append(vertices[0]) return sum( (p2.x - p1.x) * (p2.y + p1.y) for p1, p2 in zip(vertices, vertices[1:]) ) > 0
def test_polygon_mapping_vertex_count_error(points): with pytest.raises(ValueError): geo.polygon_mapping(Vec3.list(points), [])
def test_line_string_to_dxf_entity(): res = cast(LWPolyline, list(geo.dxf_entities(LINE_STRING))[0]) assert res.dxftype() == 'LWPOLYLINE' assert list(res.vertices()) == Vec3.list(EXTERIOR)