1# Copyright (c) 2020, Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, List, cast 4from itertools import chain 5from ezdxf.entities import factory 6from ezdxf.math import Vec3, OCS 7import logging 8 9if TYPE_CHECKING: 10 from ezdxf.entities import MLine, DXFGraphic, Hatch, LWPolyline 11 12__all__ = ['virtual_entities'] 13 14logger = logging.getLogger('ezdxf') 15 16 17# The MLINE geometry stored in vertices, is the final geometry, 18# scaling factor, justification and MLineStyle settings are already 19# applied. 20 21def _dxfattribs(mline): 22 attribs = mline.graphic_properties() 23 # True color value of MLINE is ignored by CAD applications: 24 if 'true_color' in attribs: 25 del attribs['true_color'] 26 return attribs 27 28 29def virtual_entities(mline: 'MLine') -> List['DXFGraphic']: 30 """ Yields 'virtual' parts of MLINE as LINE, ARC and HATCH entities. 31 32 This entities are located at the original positions, but are not stored 33 in the entity database, have no handle and are not assigned to any 34 layout. 35 """ 36 37 def filling(): 38 attribs = _dxfattribs(mline) 39 attribs['color'] = style.dxf.fill_color 40 attribs['elevation'] = ocs.from_wcs(bottom_border[0]).replace(x=0, y=0) 41 attribs['extrusion'] = mline.dxf.extrusion 42 hatch = cast('Hatch', factory.new('HATCH', dxfattribs=attribs, doc=doc)) 43 bulges: List[float] = [0.0] * (len(bottom_border) * 2) 44 points = chain( 45 ocs.points_from_wcs(bottom_border), 46 ocs.points_from_wcs(reversed(top_border)) 47 ) 48 if not closed: 49 if style.get_flag_state(style.END_ROUND): 50 bulges[len(bottom_border) - 1] = 1.0 51 if style.get_flag_state(style.START_ROUND): 52 bulges[-1] = 1.0 53 lwpoints = ((v.x, v.y, bulge) for v, bulge in zip(points, bulges)) 54 hatch.paths.add_polyline_path(lwpoints, is_closed=True) 55 return hatch 56 57 def start_cap(): 58 entities = [] 59 if style.get_flag_state(style.START_SQUARE): 60 entities.extend(create_miter(miter_points[0])) 61 if style.get_flag_state(style.START_ROUND): 62 entities.extend(round_caps(0, top_index, bottom_index)) 63 if style.get_flag_state(style.START_INNER_ARC) and \ 64 len(style.elements) > 3: 65 start_index = ordered_indices[-2] 66 end_index = ordered_indices[1] 67 entities.extend(round_caps(0, start_index, end_index)) 68 return entities 69 70 def end_cap(): 71 entities = [] 72 if style.get_flag_state(style.END_SQUARE): 73 entities.extend(create_miter(miter_points[-1])) 74 if style.get_flag_state(style.END_ROUND): 75 entities.extend(round_caps(-1, bottom_index, top_index)) 76 if style.get_flag_state(style.END_INNER_ARC) and \ 77 len(style.elements) > 3: 78 start_index = ordered_indices[1] 79 end_index = ordered_indices[-2] 80 entities.extend(round_caps(-1, start_index, end_index)) 81 return entities 82 83 def round_caps(miter_index: int, start_index: int, end_index:int): 84 color1 = style.elements[start_index].color 85 color2 = style.elements[end_index].color 86 start = ocs.from_wcs(miter_points[miter_index][start_index]) 87 end = ocs.from_wcs(miter_points[miter_index][end_index]) 88 return _arc_caps(start, end, color1, color2) 89 90 def _arc_caps(start: Vec3, end: Vec3, color1: int, color2: int): 91 attribs = _dxfattribs(mline) 92 center = start.lerp(end) 93 radius = (end - start).magnitude / 2.0 94 angle = (start - center).angle_deg 95 attribs['center'] = center 96 attribs['radius'] = radius 97 attribs['color'] = color1 98 attribs['start_angle'] = angle 99 attribs['end_angle'] = angle + (180 if color1 == color2 else 90) 100 arc1 = factory.new('ARC', dxfattribs=attribs, doc=doc) 101 if color1 == color2: 102 return arc1, 103 attribs['start_angle'] = angle + 90 104 attribs['end_angle'] = angle + 180 105 attribs['color'] = color2 106 arc2 = factory.new('ARC', dxfattribs=attribs, doc=doc) 107 return arc1, arc2 108 109 def lines(): 110 prev = None 111 _lines = [] 112 attribs = _dxfattribs(mline) 113 114 for miter in miter_points: 115 if prev is not None: 116 for index, element in enumerate(style.elements): 117 attribs['start'] = prev[index] 118 attribs['end'] = miter[index] 119 attribs['color'] = element.color 120 attribs['linetype'] = element.linetype 121 _lines.append(factory.new( 122 'LINE', dxfattribs=attribs, doc=doc)) 123 prev = miter 124 return _lines 125 126 def display_miter(): 127 _lines = [] 128 skip = set() 129 skip.add(len(miter_points) - 1) 130 if not closed: 131 skip.add(0) 132 for index, miter in enumerate(miter_points): 133 if index not in skip: 134 _lines.extend(create_miter(miter)) 135 return _lines 136 137 def create_miter(miter): 138 _lines = [] 139 attribs = _dxfattribs(mline) 140 top = miter[top_index] 141 bottom = miter[bottom_index] 142 zero = bottom.lerp(top) 143 element = style.elements[top_index] 144 attribs['start'] = top 145 attribs['end'] = zero 146 attribs['color'] = element.color 147 attribs['linetype'] = element.linetype 148 _lines.append(factory.new( 149 'LINE', dxfattribs=attribs, doc=doc)) 150 element = style.elements[bottom_index] 151 attribs['start'] = bottom 152 attribs['end'] = zero 153 attribs['color'] = element.color 154 attribs['linetype'] = element.linetype 155 _lines.append(factory.new( 156 'LINE', dxfattribs=attribs, doc=doc)) 157 return _lines 158 159 entities = [] 160 if not mline.is_alive or mline.doc is None or len(mline.vertices) < 2: 161 return entities 162 163 style = mline.style 164 if style is None: 165 return entities 166 167 doc = mline.doc 168 ocs = OCS(mline.dxf.extrusion) 169 element_count = len(style.elements) 170 closed = mline.is_closed 171 ordered_indices = style.ordered_indices() 172 bottom_index = ordered_indices[0] 173 top_index = ordered_indices[-1] 174 bottom_border: List[Vec3] = [] 175 top_border: List[Vec3] = [] 176 miter_points: List[List[Vec3]] = [] 177 178 for vertex in mline.vertices: 179 offsets = vertex.line_params 180 if len(offsets) != element_count: 181 logger.debug( 182 f'Invalid line parametrization for vertex {len(miter_points)} ' 183 f'in {str(mline)}.' 184 ) 185 return entities 186 location = vertex.location 187 miter_direction = vertex.miter_direction 188 miter = [] 189 for offset in offsets: 190 try: 191 length = offset[0] 192 except IndexError: # DXFStructureError? 193 length = 0 194 miter.append(location + miter_direction * length) 195 miter_points.append(miter) 196 top_border.append(miter[top_index]) 197 bottom_border.append(miter[bottom_index]) 198 199 if closed: 200 miter_points.append(miter_points[0]) 201 top_border.append(top_border[0]) 202 bottom_border.append(bottom_border[0]) 203 204 if not closed: 205 entities.extend(start_cap()) 206 207 entities.extend(lines()) 208 209 if style.get_flag_state(style.MITER): 210 entities.extend(display_miter()) 211 212 if not closed: 213 entities.extend(end_cap()) 214 215 if style.get_flag_state(style.FILL): 216 entities.insert(0, filling()) 217 218 return entities 219