def circular_arc(self, data: bytes): bs = ByteStream(data) attribs = self._build_dxf_attribs() attribs['center'] = Vec3(bs.read_vertex()) attribs['radius'] = bs.read_float() normal = Vec3(bs.read_vertex()) if normal != (0, 0, 1): logger.debug('ProxyGraphic: unsupported 3D ARC.') start_vec = Vec3(bs.read_vertex()) sweep_angle = bs.read_float() arc_type = bs.read_struct('L')[0] # just do 2D for now start_angle = start_vec.angle_deg end_angle = start_angle + math.degrees(sweep_angle) attribs['start_angle'] = start_angle attribs['end_angle'] = end_angle return self._factory('ARC', dxfattribs=attribs)
def rytz_axis_construction(d1: Vec3, d2: Vec3) -> Tuple[Vec3, Vec3, float]: """The Rytz’s axis construction is a basic method of descriptive Geometry to find the axes, the semi-major axis and semi-minor axis, starting from two conjugated half-diameters. Source: `Wikipedia <https://en.m.wikipedia.org/wiki/Rytz%27s_construction>`_ Given conjugated diameter `d1` is the vector from center C to point P and the given conjugated diameter `d2` is the vector from center C to point Q. Center of ellipse is always ``(0, 0, 0)``. This algorithm works for 2D/3D vectors. Args: d1: conjugated semi-major axis as :class:`Vec3` d2: conjugated semi-minor axis as :class:`Vec3` Returns: Tuple of (major axis, minor axis, ratio) """ Q = Vec3(d1) # vector CQ # calculate vector CP', location P' if math.isclose(d1.z, 0, abs_tol=1e-9) and math.isclose( d2.z, 0, abs_tol=1e-9): # Vec3.orthogonal() works only for vectors in the xy-plane! P1 = Vec3(d2).orthogonal(ccw=False) else: extrusion = d1.cross(d2) P1 = extrusion.cross(d2).normalize(d2.magnitude) D = P1.lerp(Q) # vector CD, location D, midpoint of P'Q radius = D.magnitude radius_vector = (Q - P1).normalize(radius) # direction vector P'Q A = D - radius_vector # vector CA, location A B = D + radius_vector # vector CB, location B if A.isclose(NULLVEC) or B.isclose(NULLVEC): raise ArithmeticError("Conjugated axis required, invalid source data.") major_axis_length = (A - Q).magnitude minor_axis_length = (B - Q).magnitude if math.isclose(major_axis_length, 0.0) or math.isclose( minor_axis_length, 0.0): raise ArithmeticError("Conjugated axis required, invalid source data.") ratio = minor_axis_length / major_axis_length major_axis = B.normalize(major_axis_length) minor_axis = A.normalize(minor_axis_length) return major_axis, minor_axis, ratio
def test_fmt_mapping(): from ezdxf.math import Vec3 d = {'a': 1, 'b': 'str', 'c': Vec3(), 'd': 'xxx "yyy" \'zzz\''} r = list(_fmt_mapping(d)) assert r[0] == "'a': 1," assert r[1] == "'b': \"str\"," assert r[2] == "'c': (0.0, 0.0, 0.0)," assert r[3] == "'d': \"xxx \\\"yyy\\\" 'zzz'\","
def test_arc_params_issue_708(arc_params): cpts = list(arc_params(-2.498091544796509, -0.6435011087932844)) assert cpts[0] == (Vec3(-0.8, -0.6, 0.0), Vec3(-0.6111456180001683, -0.8518058426664423, 0.0), Vec3(-0.3147573033330529, -1.0, 0.0), Vec3(6.123233995736766e-17, -1.0, 0.0)) assert cpts[1] == (Vec3(6.123233995736766e-17, -1.0, 0.0), Vec3(0.314757303333053, -1.0, 0.0), Vec3(0.6111456180001683, -0.8518058426664423, 0.0), Vec3(0.8, -0.5999999999999999, 0.0))
def test_arbitrary_ucs(): origin = Vec3(3, 3, 3) ux = Vec3(1, 2, 0) def_point_in_xy_plane = Vec3(3, 10, 4) uz = ux.cross(def_point_in_xy_plane - origin) ucs = UCS(origin=origin, ux=ux, uz=uz) m = Matrix44.ucs(ucs.ux, ucs.uy, ucs.uz, ucs.origin) def_point_in_ucs = ucs.from_wcs(def_point_in_xy_plane) assert ucs.ux == m.ux assert ucs.uy == m.uy assert ucs.uz == m.uz assert ucs.origin == m.origin assert def_point_in_ucs == m.ucs_vertex_from_wcs(def_point_in_xy_plane) assert def_point_in_ucs.z == 0 assert ucs.to_wcs(def_point_in_ucs).isclose(def_point_in_xy_plane) assert ucs.is_cartesian is True
def test_matrix44_to_wcs(): ocs = OCS(EXTRUSION) matrix = Matrix44.ucs(ocs.ux, ocs.uy, ocs.uz) assert is_close_points( matrix.ocs_to_wcs( Vec3(9.41378764657076, 13.15481838975576, 0.8689258932616031)), (-9.56460754, 8.44764172, 9.97894327), places=6, )
def rotate(vertices: Iterable['Vertex'], angle: 0., deg: bool = True) -> Iterable[Vec3]: """ Rotate `vertices` about to z-axis at to origin (0, 0), faster than a Matrix44 transformation. Args: vertices: iterable of vertices angle: rotation angle deg: True if angle in degrees, False if angle in radians Returns: yields transformed vertices """ if deg: return (Vec3(v).rotate_deg(angle) for v in vertices) else: return (Vec3(v).rotate(angle) for v in vertices)
def test_fmt_mapping(): from ezdxf.math import Vec3 d = {"a": 1, "b": "str", "c": Vec3(), "d": "xxx \"yyy\" 'zzz'"} r = list(_fmt_mapping(d)) assert r[0] == "'a': 1," assert r[1] == "'b': \"str\"," assert r[2] == "'c': (0.0, 0.0, 0.0)," assert r[3] == "'d': \"xxx \\\"yyy\\\" 'zzz'\","
def _update_location_from_mtext(text: Text, mtext: MText) -> None: # TEXT is an OCS entity, MTEXT is a WCS entity dxf = text.dxf insert = Vec3(mtext.dxf.insert) extrusion = Vec3(mtext.dxf.extrusion) text_direction = mtext.get_text_direction() if extrusion.isclose(Z_AXIS): # most common case dxf.rotation = text_direction.angle_deg else: ocs = OCS(extrusion) insert = ocs.from_wcs(insert) dxf.extrusion = extrusion.normalize() dxf.rotation = ocs.from_wcs(text_direction).angle_deg # type: ignore dxf.insert = insert dxf.align_point = insert # the same point for all MTEXT alignments! dxf.halign, dxf.valign = MAP_MTEXT_ALIGN_TO_FLAGS.get( mtext.dxf.attachment_point, (TextHAlign.LEFT, TextVAlign.TOP))
def add_dim(msp, x, y, override, dimstyle='EZ_RADIUS'): center = Vec3(x, y) msp.add_circle(center, radius=RADIUS) dim = msp.add_radius_dim(center=center, radius=RADIUS, angle=45, dimstyle=dimstyle, override=override) dim.render()
def export_dxf(layout: "GenericLayoutType", bins: List[Bin], offset: Vertex = (1, 0, 0)) -> None: from ezdxf import colors offset_vec = Vec3(offset) start = Vec3() index = 0 rgb = (colors.RED, colors.GREEN, colors.BLUE, colors.MAGENTA, colors.CYAN) for box in bins: m = Matrix44.translate(start.x, start.y, start.z) _add_frame(layout, box, "FRAME", m) for item in box.items: _add_mesh(layout, item, "ITEMS", rgb[index], m) index += 1 if index >= len(rgb): index = 0 start += offset_vec
def translate(self, dx: float, dy: float, dz: float) -> 'XLine': """ Optimized XLINE/RAY translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ self.dxf.start = Vec3(dx, dy, dz) + self.dxf.start return self
def polygons_wcs(self, ocs: OCS, elevation: float) -> Iterable[Sequence[Vec3]]: """Yields for each sub-trace a single polygon as sequence of :class:`~ezdxf.math.Vec3` objects in :ref:`WCS`. """ for trace in self._traces: yield tuple( ocs.points_to_wcs( Vec3(v.x, v.y, elevation) for v in trace.polygon()))
def transform_vertices(self, vectors: Iterable['Vertex']) -> Iterable[Vec3]: """ Returns an iterable of transformed vertices. """ m0, m1, m2, m3, m4, m5, m6, m7, m8, m9, m10, m11, m12, m13, m14, m15 = self.matrix for vector in vectors: x, y, z = vector yield Vec3(x * m0 + y * m4 + z * m8 + m12, x * m1 + y * m5 + z * m9 + m13, x * m2 + y * m6 + z * m10 + m14)
def translate(self, dx: float, dy: float, dz: float) -> 'Line': """ Optimized LINE translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis. """ vec = Vec3(dx, dy, dz) self.dxf.start = vec + self.dxf.start self.dxf.end = vec + self.dxf.end return self
def translate(self, dx: float, dy: float, dz: float) -> 'Ellipse': """ Optimized ELLIPSE translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis, returns `self` (floating interface). .. versionadded:: 0.13 """ self.dxf.center = Vec3(dx, dy, dz) + self.dxf.center return self
def test_audit_invalid_major_axis(self, msp): ellipse = msp.add_ellipse((0, 0), (1, 0)) # can only happen for loaded DXF files ellipse.dxf.__dict__["major_axis"] = Vec3(0, 0, 0) # hack auditor = Auditor(ellipse.doc) ellipse.audit(auditor) auditor.empty_trashcan() assert len(auditor.fixes) == 1 assert ellipse.is_alive is False, "invalid ellipse should be deleted"
def translate(self, dx: float, dy: float, dz: float) -> 'Point': """ Optimized POINT translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis. .. versionadded:: 0.13 """ self.dxf.location = Vec3(dx, dy, dz) + self.dxf.location return self
def baseline_vertices(self, insert: Vec3, halign: int = 0, valign: int = 0, angle: float = 0) -> List[Vec3]: """ Returns the left and the right baseline vertex of the text line. Args: insert: insertion point halign: horizontal alignment left=0, center=1, right=2 valign: vertical alignment baseline=0, bottom=1, middle=2, top=3 angle: text rotation in radians """ fm = self.font_measurements() vertices = [ Vec3(0, fm.baseline), Vec3(self.width, fm.baseline), ] shift = self._shift_vector(halign, valign, fm) return _transform(vertices, insert, shift, angle)
def _create_linked_columns(self) -> None: """ Create linked MTEXT columns for DXF versions before R2018. """ # creates virtual MTEXT entities dxf = self.dxf attribs = self.dxfattribs(drop={'handle', 'owner'}) doc = self.doc cols = self._columns insert = dxf.get('insert', Vec3()) default_direction = Vec3.from_deg_angle(dxf.get('rotation', 0)) text_direction = Vec3(dxf.get('text_direction', default_direction)) offset = text_direction.normalize(cols.width + cols.gutter_width) linked_columns = cols.linked_columns for _ in range(cols.count - 1): insert += offset column = MText.new(dxfattribs=attribs, doc=doc) column.dxf.insert = insert linked_columns.append(column)
def translate(self, dx: float, dy: float, dz: float) -> "Point": """Optimized POINT translation about `dx` in x-axis, `dy` in y-axis and `dz` in z-axis. """ self.dxf.location = Vec3(dx, dy, dz) + self.dxf.location # Avoid Matrix44 instantiation if not required: if self.is_post_transform_required: self.post_transform(Matrix44.translate(dx, dy, dz)) return self
def draw_text_entity(self, entity: DXFGraphic, properties: Properties) -> None: # Draw embedded MTEXT entity as virtual MTEXT entity: if isinstance(entity, BaseAttrib) and entity.has_embedded_mtext_entity: self.draw_mtext_entity(entity.virtual_mtext_entity(), properties) elif is_spatial_text(Vec3(entity.dxf.extrusion)): self.draw_text_entity_3d(entity, properties) else: self.draw_text_entity_2d(entity, properties)
def closest_point(base: "Vertex", points: Iterable["Vertex"]) -> "Vec3": """Returns closest point to `base`. Args: base: base point as :class:`Vec3` compatible object points: iterable of points as :class:`Vec3` compatible object """ base = Vec3(base) min_dist = None found = None for point in points: p = Vec3(point) dist = (base - p).magnitude if (min_dist is None) or (dist < min_dist): min_dist = dist found = p return found
def add_dim(x, y, radius, dimtad): center = Vec3(x, y) msp.add_circle((x, y), radius=3) dim_location = center + Vec3.from_deg_angle(angle, radius) dim = msp.add_diameter_dim(center=(x, y), radius=3, location=dim_location, dimstyle='EZ_RADIUS', override={ 'dimtad': dimtad, }) dim.render(discard=BRICSCAD)
def test_transform_mtext_with_linked_columns(): mtext = new_mtext_with_linked_columns(3) offset = Vec3(1, 2, 3) mtext2 = mtext.copy() mtext2.translate(*offset.xyz) assert mtext2.dxf.insert.isclose(mtext.dxf.insert + offset) for col1, col2 in zip(mtext.columns.linked_columns, mtext2.columns.linked_columns): assert col2.dxf.insert.isclose(col1.dxf.insert + offset)
def user_location_override(self, location: 'Vertex') -> None: """ Set text location by user, `location` is relative to the origin of the UCS defined in the :meth:`render` method or WCS if the `ucs` argument is ``None``. """ self.dimension.set_flag_state(self.dimension.USER_LOCATION_OVERRIDE, state=True, name='dimtype') self.dimstyle_attribs['user_location'] = Vec3(location)
def moveto(self, location: "Vertex") -> "UCS": """Place current UCS at new origin `location` and returns `self`. Args: location: new origin in WCS """ self.origin = Vec3(location) return self
def shift(self, delta: "Vertex") -> "UCS": """Shifts current UCS by `delta` vector and returns `self`. Args: delta: shifting vector """ self.origin += Vec3(delta) return self
def draw_polyline_entity(self, entity: DXFGraphic, properties: Properties) -> None: dxftype = entity.dxftype() if dxftype == 'POLYLINE': e = cast(Polyface, entity) if e.is_polygon_mesh or e.is_poly_face_mesh: # draw 3D mesh or poly-face entity self.draw_mesh_builder_entity( MeshBuilder.from_polyface(e), properties, ) return entity = cast(Union[LWPolyline, Polyline], entity) is_lwpolyline = dxftype == 'LWPOLYLINE' if entity.has_width: # draw banded 2D polyline elevation = 0.0 ocs = entity.ocs() transform = ocs.transform if transform: if is_lwpolyline: # stored as float elevation = entity.dxf.elevation else: # stored as vector (0, 0, elevation) elevation = Vec3(entity.dxf.elevation).z trace = TraceBuilder.from_polyline( entity, segments=self.circle_approximation_count // 2 ) for polygon in trace.polygons(): # polygon is a sequence of Vec2() if transform: points = ocs.points_to_wcs( Vec3(v.x, v.y, elevation) for v in polygon ) else: points = Vec3.generate(polygon) # Set default SOLID filling for LWPOLYLINE properties.filling = Filling() self.out.draw_filled_polygon(points, properties) return path = Path.from_lwpolyline(entity) \ if is_lwpolyline else Path.from_polyline(entity) self.out.draw_path(path, properties)
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