def draw_elliptic_arc_entity(self, entity: DXFGraphic, properties: Properties) -> None: dxftype = entity.dxftype() if NULLVEC.isclose(entity.dxf.extrusion): self.skip_entity( f'Invalid extrusion (0, 0, 0) in entity: {str(entity)}') return if dxftype == 'CIRCLE': if entity.dxf.radius <= 0: self.skip_entity(f'Invalid radius in entity: {str(entity)}') return path = Path.from_circle(cast('Circle', entity)) elif dxftype == 'ARC': if entity.dxf.radius <= 0: self.skip_entity(f'Invalid radius in entity: {str(entity)}') return path = Path.from_arc(cast('Arc', entity)) elif dxftype == 'ELLIPSE': if NULLVEC.isclose(entity.dxf.major_axis): self.skip_entity( f'Invalid major axis (0, 0, 0) in entity: {str(entity)}') return path = Path.from_ellipse(cast('Ellipse', entity)) else: # API usage error raise TypeError(dxftype) self.out.draw_path(path, properties)
def _convert_entity(self): """ Calculates the rough border path for a single line text. Calculation is based on a mono-spaced font and therefore the border path is just an educated guess. Vertical text generation and oblique angle is ignored. """ def get_text_rotation() -> float: if alignment in ('FIT', 'ALIGNED') and not p1.isclose(p2): return (p2 - p1).angle else: return math.degrees(text.dxf.rotation) def get_insert() -> Vec3: if alignment == 'LEFT': return p1 elif alignment in ('FIT', 'ALIGNED'): return p1.lerp(p2, factor=0.5) else: return p2 text = cast('Text', self.entity) if text.dxftype() == 'ATTDEF': # ATTDEF outside of a BLOCK renders the tag rather than the value content = text.dxf.tag else: content = text.dxf.text content = plain_text(content) if len(content) == 0: # empty path - does not render any vertices! self._path = Path() return p1: Vec3 = text.dxf.insert p2: Vec3 = text.dxf.align_point font = fonts.make_font(get_font_name(text), text.dxf.height, text.dxf.width) text_line = TextLine(content, font) alignment: str = text.get_align() if text.dxf.halign > 2: # ALIGNED=3, MIDDLE=4, FIT=5 text_line.stretch(alignment, p1, p2) halign, valign = unified_alignment(text) corner_vertices = text_line.corner_vertices(get_insert(), halign, valign, get_text_rotation()) ocs = text.ocs() self._path = Path.from_vertices( ocs.points_to_wcs(corner_vertices), close=True, )
def mapping(entity: DXFGraphic, distance: float = MAX_FLATTENING_DISTANCE, force_line_string: bool = False) -> Dict: """ Create the compiled ``__geo_interface__`` mapping as :class:`dict` for the given DXF `entity`, all coordinates are :class:`Vector` objects and represents "Polygon" always as tuple (exterior, holes) even without holes. Internal API - result is **not** a valid ``_geo_interface__`` mapping! Args: entity: DXF entity distance: maximum flattening distance for curve approximations force_line_string: by default this function returns Polygon objects for closed geometries like CIRCLE, SOLID, closed POLYLINE and so on, by setting argument `force_line_string` to ``True``, this entities will be returned as LineString objects. """ dxftype = entity.dxftype() if dxftype == 'POINT': return {TYPE: POINT, COORDINATES: entity.dxf.location} elif dxftype == 'LINE': return line_string_mapping([entity.dxf.start, entity.dxf.end]) elif dxftype == 'POLYLINE': entity = cast('Polyline', entity) if entity.is_3d_polyline or entity.is_2d_polyline: # May contain arcs as bulge values: path = Path.from_polyline(entity) points = list(path.flattening(distance)) return _line_string_or_polygon_mapping(points, force_line_string) else: raise TypeError('Polymesh and Polyface not supported.') elif dxftype == 'LWPOLYLINE': # May contain arcs as bulge values: path = Path.from_lwpolyline(cast('LWPolyline', entity)) points = list(path.flattening(distance)) return _line_string_or_polygon_mapping(points, force_line_string) elif dxftype in {'CIRCLE', 'ARC', 'ELLIPSE', 'SPLINE'}: return _line_string_or_polygon_mapping( list(entity.flattening(distance)), force_line_string) elif dxftype in {'SOLID', 'TRACE', '3DFACE'}: return _line_string_or_polygon_mapping(entity.wcs_vertices(close=True), force_line_string) elif dxftype == 'HATCH': return _hatch_as_polygon(entity, distance, force_line_string) else: raise TypeError(dxftype)
def _boundaries_to_polygons(boundaries, ocs, elevation): paths = (Path.from_hatch_boundary_path(boundary, ocs, elevation) for boundary in boundaries) for polygon in nesting.fast_bbox_detection(paths): exterior = polygon[0] # only take exterior path of level 1 holes, nested holes are ignored yield exterior, [hole[0] for hole in polygon[1:]]
def test_end_points(ellipse): p = Path.from_ellipse(ellipse) assert ellipse.start_point.isclose(p.start) assert ellipse.end_point.isclose(p.end) # end point locations measured in BricsCAD: assert ellipse.start_point.isclose((2191.3054, -1300.8375), abs_tol=1e-4) assert ellipse.end_point.isclose((2609.7870, -1520.6677), abs_tol=1e-4)
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 _hatch_primitives( hatch: 'Hatch', max_flattening_distance=None) -> Iterable[AbstractPrimitive]: """ Yield all HATCH boundary paths as separated Path() objects. """ ocs = hatch.ocs() elevation = hatch.dxf.elevation.z for boundary in hatch.paths: yield PathPrimitive( Path.from_hatch_boundary_path(boundary, ocs, elevation), hatch, max_flattening_distance)
def path(self) -> Optional[Path]: """ Create path representation on demand. :class:`Path` can not represent a point, a :class:`Path` with only a start point yields not vertices! """ if self._path is None: self._path = Path(self.entity.dxf.location) return self._path
def draw_solid_entity(self, entity: DXFGraphic) -> None: # Handles SOLID, TRACE and 3DFACE dxf, dxftype = entity.dxf, entity.dxftype() properties = self._resolve_properties(entity) points = get_tri_or_quad_points(entity, adjust_order=dxftype != '3DFACE') # TRACE is an OCS entity if dxftype == 'TRACE' and dxf.hasattr('extrusion'): ocs = entity.ocs() points = list(ocs.points_to_wcs(points)) if dxftype == '3DFACE': self.out.draw_path(Path.from_vertices(points, close=True), properties) else: # SOLID, TRACE self.out.draw_filled_polygon(points, properties)
def linedrawer(painter, scaler, boardlen, boardwid): # Right now this is disabled as I am just testing the Hatch drawing """for e in msp: print(e.dxftype())""" """for e in msp.query("CIRCLE"): ocs = e.ocs() radius = e.dxf.radius wcs_center = ocs.to_wcs(e.dxf.center) painter.drawEllipse(wcs_center[0] * scaler, wcs_center[1] * scaler, (radius / 2) * scaler, (radius / 2) * scaler)""" for e in msp.query('LINE'): if e.dxf.layer in render_layers: start = e.dxf.start end = e.dxf.end startx = start[0] * scaler starty = start[1] * scaler endx = end[0] * scaler endy = end[1] * scaler # Right now this is disabled as I am just testing the Hatch drawing # painter.drawLine(startx, starty, endx, endy) for hatch in msp: try: if hatch.dxftype() == "HATCH" and hatch.dxf.layer in render_layers: ocs = hatch.ocs() for p in hatch.paths: # For each path of a hatch if p.PATH_TYPE == 'EdgePath': path = Path.from_hatch_edge_path(p, ocs, 0) for counter, vector in enumerate(path.approximate()): if counter == 0: oldvector = vector sxs = vector[0] * scaler sys = (boardlen - vector[1]) * scaler sxe = oldvector[0] * scaler sye = (boardlen - oldvector[1]) * scaler oldvector = vector painter.drawLine(sxs, sys, sxe, sye) except: print("Error!") continue for region in msp: try: if region.dxftype( ) == "REGION" and region.dxf.layer in render_layers: print("REGION found") except: print("Error!") continue
# Copyright (c) 2020, Manfred Moitzi # License: MIT License import ezdxf from ezdxf.render import Path doc = ezdxf.new() msp = doc.modelspace() ellipse = msp.add_ellipse( center=(1999.488177113287, -1598.02265357955, 0.0), major_axis=(629.968069297, 0.0, 0.0), ratio=0.495263197, start_param=-1.261396328799999, end_param=-0.2505454928, dxfattribs={ 'layer': "0", 'linetype': "Continuous", 'color': 3, 'extrusion': (0.0, 0.0, -1.0), }, ) p = Path.from_ellipse(ellipse) msp.add_lwpolyline(p.approximate(), dxfattribs={ 'layer': 'PathRendering', 'color': 1, }) doc.set_modelspace_vport(500, (2400, -1400)) doc.saveas('path_rendering.dxf')
def _convert_entity(self): """ Calculates the rough border path for a MTEXT entity. Calculation is based on a mono-spaced font and therefore the border path is just an educated guess. Most special features of MTEXT is not supported. """ def get_content() -> List[str]: text = mtext.plain_text(split=False) return text_wrap(text, box_width, font.text_width) def get_max_str() -> str: return max(content, key=lambda s: len(s)) def get_rect_width() -> float: if box_width: return box_width s = get_max_str() if len(s) == 0: s = " " return font.text_width(s) def get_rect_height() -> float: line_height = font.measurements.total_height cap_height = font.measurements.cap_height # Line spacing factor: Percentage of default (3-on-5) line # spacing to be applied. # thx to mbway: multiple of cap_height between the baseline of the # previous line and the baseline of the next line # 3-on-5 line spacing = 5/3 = 1.67 line_spacing = cap_height * mtext.dxf.line_spacing_factor * 1.67 spacing = line_spacing - line_height line_count = len(content) return line_height * line_count + spacing * (line_count - 1) def get_ucs() -> UCS: """ Create local coordinate system: origin = insertion point z-axis = extrusion vector x-axis = text_direction or text rotation, text rotation requires extrusion vector == (0, 0, 1) or treatment like an OCS? """ origin = mtext.dxf.insert z_axis = mtext.dxf.extrusion # default is Z_AXIS x_axis = X_AXIS if mtext.dxf.hasattr('text_direction'): x_axis = mtext.dxf.text_direction elif mtext.dxf.hasattr('rotation'): # TODO: what if extrusion vector is not (0, 0, 1) x_axis = Vec3.from_deg_angle(mtext.dxf.rotation) z_axis = Z_AXIS return UCS(origin=origin, ux=x_axis, uz=z_axis) def get_shift_factors(): halign, valign = unified_alignment(mtext) shift_x = 0 shift_y = 0 if halign == const.CENTER: shift_x = -0.5 elif halign == const.RIGHT: shift_x = -1.0 if valign == const.MIDDLE: shift_y = 0.5 elif valign == const.BOTTOM: shift_y = 1.0 return shift_x, shift_y def get_corner_vertices() -> Iterable[Vec3]: """ Create corner vertices in the local working plan, where the insertion point is the origin. """ rect_width = mtext.dxf.get('rect_width', get_rect_width()) rect_height = mtext.dxf.get('rect_height', get_rect_height()) # TOP LEFT alignment: vertices = [ Vec3(0, 0), Vec3(rect_width, 0), Vec3(rect_width, -rect_height), Vec3(0, -rect_height) ] sx, sy = get_shift_factors() shift = Vec3(sx * rect_width, sy * rect_height) return (v + shift for v in vertices) mtext: "MText" = cast("MText", self.entity) box_width = mtext.dxf.get('width', 0) font = fonts.make_font(get_font_name(mtext), mtext.dxf.char_height, 1.0) content: List[str] = get_content() if len(content) == 0: # empty path - does not render any vertices! self._path = Path() return ucs = get_ucs() corner_vertices = get_corner_vertices() self._path = Path.from_vertices( ucs.points_to_wcs(corner_vertices), close=True, )
def boundary_to_vertices(boundary) -> List[Vector]: path = Path.from_hatch_boundary_path(boundary, ocs, elevation) return path_to_vertices(path)
# Copyright (c) 2020, Manfred Moitzi # License: MIT License import pytest from ezdxf.render.forms import square, translate from ezdxf.render import Path, nesting EXTERIOR = list(translate(square(10), (-5, -5))) EXT1_PATH = Path.from_vertices(EXTERIOR) EXT2_PATH = Path.from_vertices(translate(EXTERIOR, (11, 0))) CENTER_HOLE1 = list(translate(square(8), (-4, -4))) CH1_PATH = Path.from_vertices(CENTER_HOLE1) CENTER_HOLE2 = list(translate(square(6), (-3, -3))) CH2_PATH = Path.from_vertices(CENTER_HOLE2) LEFT_HOLE = list(translate(square(2.1), (-3, -1))) LH_PATH = Path.from_vertices(LEFT_HOLE) RIGHT_HOLE = list(translate(square(2.0), (3, -1))) RH_PATH = Path.from_vertices(RIGHT_HOLE) DETECTION_DATA = [ pytest.param( # Each polygon is a list of paths [EXT1_PATH], [[EXT1_PATH]], id='1 path'), pytest.param( # returns the path sorted by area, and reversed if equal sized [EXT1_PATH, EXT2_PATH], [[EXT2_PATH], [EXT1_PATH]],
def to_path(p): path = Path.from_hatch_boundary_path(p, ocs, elevation) path.close() return path
def draw_spline_entity(self, entity: DXFGraphic) -> None: properties = self._resolve_properties(entity) path = Path.from_spline(cast(Spline, entity)) self.out.draw_path(path, properties)