1# Copyright (c) 2019-2020 Manfred Moitzi 2# License: MIT License 3from typing import TYPE_CHECKING, Optional, Tuple, Iterable, Dict 4from ezdxf.entities import factory 5from ezdxf import options 6from ezdxf.lldxf import validator 7from ezdxf.lldxf.attributes import ( 8 DXFAttr, DXFAttributes, DefSubclass, RETURN_DEFAULT, group_code_mapping, 9) 10from ezdxf import colors as clr 11from ezdxf.lldxf.const import ( 12 DXF12, DXF2000, DXF2004, DXF2007, DXF2013, DXFValueError, DXFKeyError, 13 DXFTableEntryError, SUBCLASS_MARKER, DXFInvalidLineType, DXFStructureError, 14) 15from ezdxf.math import OCS, Matrix44 16from ezdxf.proxygraphic import load_proxy_graphic, export_proxy_graphic 17from .dxfentity import DXFEntity, base_class, SubclassProcessor 18 19if TYPE_CHECKING: 20 from ezdxf.eztypes import ( 21 Auditor, TagWriter, BaseLayout, DXFNamespace, Vertex, Drawing, 22 ) 23 24__all__ = [ 25 'DXFGraphic', 'acdb_entity', 'SeqEnd', 'add_entity', 26 'replace_entity', 'elevation_to_z_axis', 27] 28 29GRAPHIC_PROPERTIES = { 30 'layer', 'linetype', 'color', 'lineweight', 'ltscale', 'true_color', 31 'color_name', 'transparency', 32} 33 34acdb_entity = DefSubclass('AcDbEntity', { 35 # Layer name as string, no auto fix for invalid names! 36 'layer': DXFAttr(8, default='0', validator=validator.is_valid_layer_name), 37 38 # Linetype name as string, no auto fix for invalid names! 39 'linetype': DXFAttr( 40 6, default='BYLAYER', optional=True, 41 validator=validator.is_valid_table_name, 42 ), 43 # ACI color index, BYBLOCK=0, BYLAYER=256, BYOBJECT=257: 44 'color': DXFAttr( 45 62, default=256, optional=True, 46 validator=validator.is_valid_aci_color, 47 fixer=RETURN_DEFAULT, 48 ), 49 # modelspace=0, paperspace=1 50 'paperspace': DXFAttr( 51 67, default=0, optional=True, 52 validator=validator.is_integer_bool, 53 fixer=RETURN_DEFAULT, 54 ), 55 56 # Lineweight in mm times 100 (e.g. 0.13mm = 13). Smallest line weight is 13 57 # and biggest line weight is 200, values outside this range prevents AutoCAD 58 # from loading the file. 59 # Special values: BYLAYER=-1, BYBLOCK=-2, DEFAULT=-3 60 'lineweight': DXFAttr( 61 370, default=-1, dxfversion=DXF2000, optional=True, 62 validator=validator.is_valid_lineweight, 63 fixer=validator.fix_lineweight, 64 ), 65 'ltscale': DXFAttr( 66 48, default=1.0, dxfversion=DXF2000, optional=True, 67 validator=validator.is_positive, 68 fixer=RETURN_DEFAULT, 69 ), 70 # visible=0, invisible=1 71 'invisible': DXFAttr(60, default=0, dxfversion=DXF2000, optional=True), 72 73 # True color as 0x00RRGGBB 24-bit value 74 # True color always overrides ACI "color"! 75 76 'true_color': DXFAttr(420, dxfversion=DXF2004, optional=True), 77 78 # Color name as string. Color books are stored in .stb config files? 79 'color_name': DXFAttr(430, dxfversion=DXF2004, optional=True), 80 81 # Transparency value 0x020000TT 0 = fully transparent / 255 = opaque 82 'transparency': DXFAttr(440, dxfversion=DXF2004, optional=True), 83 84 # Shadow mode: 85 # 0 = Casts and receives shadows 86 # 1 = Casts shadows 87 # 2 = Receives shadows 88 # 3 = Ignores shadows 89 'shadow_mode': DXFAttr(284, dxfversion=DXF2007, optional=True), 90 'material_handle': DXFAttr(347, dxfversion=DXF2007, optional=True), 91 'visualstyle_handle': DXFAttr(348, dxfversion=DXF2007, optional=True), 92 93 # PlotStyleName type enum (AcDb::PlotStyleNameType). Stored and moved around 94 # as a 16-bit integer. Custom non-entity 95 'plotstyle_enum': DXFAttr(380, dxfversion=DXF2007, default=1, 96 optional=True), 97 98 # Handle value of the PlotStyleName object, basically a hard pointer, but 99 # has a different range to make backward compatibility easier to deal with. 100 'plotstyle_handle': DXFAttr(390, dxfversion=DXF2007, optional=True), 101 102 # 92 or 160?: Number of bytes in the proxy entity graphics represented in 103 # the subsequent 310 groups, which are binary chunk records (optional) 104 # 310: Proxy entity graphics data (multiple lines; 256 characters max. per 105 # line) (optional), compiled by TagCompiler() to a DXFBinaryTag() objects 106}) 107acdb_entity_group_codes = group_code_mapping(acdb_entity) 108 109 110def elevation_to_z_axis(dxf: 'DXFNamespace', names: Iterable[str]): 111 # The elevation group code (38) is only used for DXF R11 and prior and 112 # ignored for DXF R2000 and later. 113 # DXF R12 and later store the entity elevation in the z-axis of the 114 # vertices, but AutoCAD supports elevation for R12 if no z-axis is present. 115 # DXF types with legacy elevation support: 116 # SOLID, TRACE, TEXT, CIRCLE, ARC, TEXT, ATTRIB, ATTDEF, INSERT, SHAPE 117 118 # The elevation is only used for DXF R12 if no z-axis is stored in the DXF 119 # file. This is a problem because ezdxf loads the vertices always as 3D 120 # vertex including a z-axis even if no z-axis is present in DXF file. 121 if dxf.hasattr('elevation'): 122 elevation = dxf.elevation 123 # ezdxf does not export the elevation attribute for any DXF version 124 dxf.discard('elevation') 125 if elevation == 0: 126 return 127 128 for name in names: 129 v = dxf.get(name) 130 # Only use elevation value if z-axis is 0, this will not work for 131 # situations where an elevation and a z-axis=0 is present, but let's 132 # assume if the elevation group code is used the z-axis is not 133 # present if z-axis is 0. 134 if v is not None and v.z == 0: 135 dxf.set(name, v.replace(z=elevation)) 136 137 138class DXFGraphic(DXFEntity): 139 """ Common base class for all graphic entities, a subclass of 140 :class:`~ezdxf.entities.dxfentity.DXFEntity`. These entities resides in 141 entity spaces like modelspace, paperspace or block. 142 143 """ 144 DXFTYPE = 'DXFGFX' 145 DEFAULT_ATTRIBS = {'layer': '0'} 146 DXFATTRIBS = DXFAttributes(base_class, acdb_entity) 147 148 def load_dxf_attribs( 149 self, processor: SubclassProcessor = None) -> 'DXFNamespace': 150 """ Adds subclass processing for 'AcDbEntity', requires previous base 151 class processing by parent class. 152 153 (internal API) 154 """ 155 dxf = super().load_dxf_attribs(processor) 156 if processor is None: 157 return dxf 158 r12 = processor.r12 159 # It is valid to mix up the base class with AcDbEntity class. 160 processor.append_base_class_to_acdb_entity() 161 162 # Load proxy graphic data if requested 163 if options.load_proxy_graphics: 164 # length tag has group code 92 until DXF R2010 165 if processor.dxfversion and processor.dxfversion < DXF2013: 166 code = 92 167 else: 168 code = 160 169 self.proxy_graphic = load_proxy_graphic( 170 processor.subclasses[0 if r12 else 1], 171 length_code=code, 172 ) 173 processor.fast_load_dxfattribs(dxf, acdb_entity_group_codes, 1) 174 return dxf 175 176 def post_new_hook(self): 177 """ Post processing and integrity validation after entity creation 178 (internal API) 179 """ 180 if self.doc: 181 if self.dxf.linetype not in self.doc.linetypes: 182 raise DXFInvalidLineType( 183 f'Linetype "{self.dxf.linetype}" not defined.' 184 ) 185 186 @property 187 def rgb(self) -> Optional[clr.RGB]: 188 """ Returns RGB true color as (r, g, b) tuple or None if true_color is 189 not set. 190 """ 191 if self.dxf.hasattr('true_color'): 192 return clr.int2rgb(self.dxf.get('true_color')) 193 else: 194 return None 195 196 @rgb.setter 197 def rgb(self, rgb: clr.RGB) -> None: 198 """ Set RGB true color as (r, g , b) tuple e.g. (12, 34, 56). """ 199 self.dxf.set('true_color', clr.rgb2int(rgb)) 200 201 @property 202 def transparency(self) -> float: 203 """ Get transparency as float value between 0 and 1, 0 is opaque and 1 204 is 100% transparent (invisible). 205 """ 206 if self.dxf.hasattr('transparency'): 207 return clr.transparency2float(self.dxf.get('transparency')) 208 else: 209 return 0. 210 211 @transparency.setter 212 def transparency(self, transparency: float) -> None: 213 """ Set transparency as float value between 0 and 1, 0 is opaque and 1 214 is 100% transparent (invisible). 215 """ 216 self.dxf.set('transparency', clr.float2transparency(transparency)) 217 218 def graphic_properties(self) -> Dict: 219 """ Returns the important common properties layer, color, linetype, 220 lineweight, ltscale, true_color and color_name as `dxfattribs` dict. 221 222 """ 223 attribs = dict() 224 for key in GRAPHIC_PROPERTIES: 225 if self.dxf.hasattr(key): 226 attribs[key] = self.dxf.get(key) 227 return attribs 228 229 def ocs(self) -> Optional[OCS]: 230 """ Returns object coordinate system (:ref:`ocs`) for 2D entities like 231 :class:`Text` or :class:`Circle`, returns ``None`` for entities without 232 OCS support. 233 234 """ 235 # extrusion is only defined for 2D entities like Text, Circle, ... 236 if self.dxf.is_supported('extrusion'): 237 extrusion = self.dxf.get('extrusion', default=(0, 0, 1)) 238 return OCS(extrusion) 239 else: 240 return None 241 242 def set_owner(self, owner: str, paperspace: int = 0) -> None: 243 """ Set owner attribute and paperspace flag. (internal API)""" 244 self.dxf.owner = owner 245 if paperspace: 246 self.dxf.paperspace = paperspace 247 else: 248 self.dxf.discard('paperspace') 249 250 def link_entity(self, entity: 'DXFEntity') -> None: 251 """ Store linked or attached entities. Same API for both types of 252 appended data, because entities with linked entities (POLYLINE, INSERT) 253 have no attached entities and vice versa. 254 255 (internal API) 256 """ 257 pass 258 259 def export_entity(self, tagwriter: 'TagWriter') -> None: 260 """ Export entity specific data as DXF tags. (internal API)""" 261 # Base class export is done by parent class. 262 self.export_acdb_entity(tagwriter) 263 # XDATA and embedded objects export is also done by the parent class. 264 265 def export_acdb_entity(self, tagwriter: 'TagWriter'): 266 """ Export subclass 'AcDbEntity' as DXF tags. (internal API)""" 267 # Full control over tag order and YES, sometimes order matters 268 not_r12 = tagwriter.dxfversion > DXF12 269 if not_r12: 270 tagwriter.write_tag2(SUBCLASS_MARKER, acdb_entity.name) 271 272 self.dxf.export_dxf_attribs(tagwriter, [ 273 'paperspace', 'layer', 'linetype', 'material_handle', 'color', 274 'lineweight', 'ltscale', 'true_color', 'color_name', 'transparency', 275 'plotstyle_enum', 'plotstyle_handle', 'shadow_mode', 276 'visualstyle_handle', 277 ]) 278 279 if self.proxy_graphic and not_r12 and options.store_proxy_graphics: 280 # length tag has group code 92 until DXF R2010 281 export_proxy_graphic( 282 self.proxy_graphic, 283 tagwriter=tagwriter, 284 length_code=(92 if tagwriter.dxfversion < DXF2013 else 160) 285 ) 286 287 def get_layout(self) -> Optional['BaseLayout']: 288 """ Returns the owner layout or returns ``None`` if entity is not 289 assigned to any layout. 290 """ 291 if self.dxf.owner is None: # unlinked entity 292 return None 293 try: 294 return self.doc.layouts.get_layout_by_key(self.dxf.owner) 295 except DXFKeyError: 296 pass 297 try: 298 return self.doc.blocks.get_block_layout_by_handle(self.dxf.owner) 299 except DXFTableEntryError: 300 return None 301 302 def unlink_from_layout(self) -> None: 303 """ 304 Unlink entity from associated layout. Does nothing if entity is already 305 unlinked. 306 307 It is more efficient to call the 308 :meth:`~ezdxf.layouts.BaseLayout.unlink_entity` method of the associated 309 layout, especially if you have to unlink more than one entity. 310 311 """ 312 if not self.is_alive: 313 raise TypeError('Can not unlink destroyed entity.') 314 315 if self.doc is None: 316 # no doc -> no layout 317 self.dxf.owner = None 318 return 319 320 layout = self.get_layout() 321 if layout: 322 layout.unlink_entity(self) 323 324 def move_to_layout(self, layout: 'BaseLayout', 325 source: 'BaseLayout' = None) -> None: 326 """ 327 Move entity from model space or a paper space layout to another layout. 328 For block layout as source, the block layout has to be specified. Moving 329 between different DXF drawings is not supported. 330 331 Args: 332 layout: any layout (model space, paper space, block) 333 source: provide source layout, faster for DXF R12, if entity is 334 in a block layout 335 336 Raises: 337 DXFStructureError: for moving between different DXF drawings 338 339 """ 340 if source is None: 341 source = self.get_layout() 342 if source is None: 343 raise DXFValueError('Source layout for entity not found.') 344 source.move_to_layout(self, layout) 345 346 def copy_to_layout(self, layout: 'BaseLayout') -> 'DXFEntity': 347 """ 348 Copy entity to another `layout`, returns new created entity as 349 :class:`DXFEntity` object. Copying between different DXF drawings is 350 not supported. 351 352 Args: 353 layout: any layout (model space, paper space, block) 354 355 Raises: 356 DXFStructureError: for copying between different DXF drawings 357 358 """ 359 if self.doc != layout.doc: 360 raise DXFStructureError( 361 'Copying between different DXF drawings is not supported.' 362 ) 363 364 new_entity = self.copy() 365 layout.add_entity(new_entity) 366 return new_entity 367 368 def audit(self, auditor: 'Auditor') -> None: 369 """ Audit and repair graphical DXF entities. 370 371 .. important:: 372 373 Do not delete entities while auditing process, because this 374 would alter the entity database while iterating, instead use:: 375 376 auditor.trash(entity.dxf.handle) 377 378 to delete invalid entities after auditing automatically. 379 380 """ 381 assert self.doc is auditor.doc, 'Auditor for different DXF document.' 382 if not self.is_alive: 383 return 384 385 super().audit(auditor) 386 auditor.check_owner_exist(self) 387 dxf = self.dxf 388 if dxf.hasattr('layer'): 389 auditor.check_for_valid_layer_name(self) 390 if dxf.hasattr('linetype'): 391 auditor.check_entity_linetype(self) 392 if dxf.hasattr('color'): 393 auditor.check_entity_color_index(self) 394 if dxf.hasattr('lineweight'): 395 auditor.check_entity_lineweight(self) 396 if dxf.hasattr('extrusion'): 397 auditor.check_extrusion_vector(self) 398 399 def transform(self, m: 'Matrix44') -> 'DXFGraphic': 400 """ Inplace transformation interface, returns `self` 401 (floating interface). 402 403 Args: 404 m: 4x4 transformation matrix (:class:`ezdxf.math.Matrix44`) 405 406 """ 407 raise NotImplementedError() 408 409 def translate(self, dx: float, dy: float, dz: float) -> 'DXFGraphic': 410 """ Translate entity inplace about `dx` in x-axis, `dy` in y-axis and 411 `dz` in z-axis, returns `self` (floating interface). 412 413 Basic implementation uses the :meth:`transform` interface, subclasses 414 may have faster implementations. 415 416 """ 417 return self.transform(Matrix44.translate(dx, dy, dz)) 418 419 def scale(self, sx: float, sy: float, sz: float) -> 'DXFGraphic': 420 """ Scale entity inplace about `dx` in x-axis, `dy` in y-axis and `dz` 421 in z-axis, returns `self` (floating interface). 422 423 """ 424 return self.transform(Matrix44.scale(sx, sy, sz)) 425 426 def scale_uniform(self, s: float) -> 'DXFGraphic': 427 """ Scale entity inplace uniform about `s` in x-axis, y-axis and z-axis, 428 returns `self` (floating interface). 429 430 """ 431 return self.transform(Matrix44.scale(s)) 432 433 def rotate_axis(self, axis: 'Vertex', angle: float) -> 'DXFGraphic': 434 """ Rotate entity inplace about vector `axis`, returns `self` 435 (floating interface). 436 437 Args: 438 axis: rotation axis as tuple or :class:`Vec3` 439 angle: rotation angle in radians 440 441 """ 442 return self.transform(Matrix44.axis_rotate(axis, angle)) 443 444 def rotate_x(self, angle: float) -> 'DXFGraphic': 445 """ Rotate entity inplace about x-axis, returns `self` 446 (floating interface). 447 448 Args: 449 angle: rotation angle in radians 450 451 """ 452 return self.transform(Matrix44.x_rotate(angle)) 453 454 def rotate_y(self, angle: float) -> 'DXFGraphic': 455 """ Rotate entity inplace about y-axis, returns `self` 456 (floating interface). 457 458 Args: 459 angle: rotation angle in radians 460 461 """ 462 return self.transform(Matrix44.y_rotate(angle)) 463 464 def rotate_z(self, angle: float) -> 'DXFGraphic': 465 """ Rotate entity inplace about z-axis, returns `self` 466 (floating interface). 467 468 Args: 469 angle: rotation angle in radians 470 471 """ 472 return self.transform(Matrix44.z_rotate(angle)) 473 474 def has_hyperlink(self) -> bool: 475 """ Returns ``True`` if entity has an attached hyperlink. """ 476 return bool(self.xdata) and ('PE_URL' in self.xdata) 477 478 def set_hyperlink(self, link: str, description: str = None, 479 location: str = None): 480 """ Set hyperlink of an entity. """ 481 xdata = [(1001, 'PE_URL'), (1000, str(link))] 482 if description: 483 xdata.append((1002, '{')) 484 xdata.append((1000, str(description))) 485 if location: 486 xdata.append((1000, str(location))) 487 xdata.append((1002, '}')) 488 489 self.discard_xdata('PE_URL') 490 self.set_xdata('PE_URL', xdata) 491 if self.doc and 'PE_URL' not in self.doc.appids: 492 self.doc.appids.new('PE_URL') 493 return self 494 495 def get_hyperlink(self) -> Tuple[str, str, str]: 496 """ Returns hyperlink, description and location. """ 497 link = "" 498 description = "" 499 location = "" 500 if self.xdata and 'PE_URL' in self.xdata: 501 xdata = [tag.value for tag in self.get_xdata('PE_URL') if 502 tag.code == 1000] 503 if len(xdata): 504 link = xdata[0] 505 if len(xdata) > 1: 506 description = xdata[1] 507 if len(xdata) > 2: 508 location = xdata[2] 509 return link, description, location 510 511 def remove_dependencies(self, other: 'Drawing' = None) -> None: 512 """ Remove all dependencies from current document. 513 514 (internal API) 515 """ 516 if not self.is_alive: 517 return 518 519 super().remove_dependencies(other) 520 # The layer attribute is preserved because layer doesn't need a layer 521 # table entry, the layer attributes are reset to default attributes 522 # like color is 7 and linetype is CONTINUOUS 523 has_linetype = (bool(other) and self.dxf.linetype in other.linetypes) 524 if not has_linetype: 525 self.dxf.linetype = 'BYLAYER' 526 self.dxf.discard('material_handle') 527 self.dxf.discard('visualstyle_handle') 528 self.dxf.discard('plotstyle_enum') 529 self.dxf.discard('plotstyle_handle') 530 531 def _new_compound_entity(self, type_: str, 532 dxfattribs: dict) -> 'DXFGraphic': 533 """ Create and bind new entity with same layout settings as `self`. 534 535 Used by INSERT & POLYLINE to create appended DXF entities, don't use it 536 to create new standalone entities. 537 538 (internal API) 539 """ 540 dxfattribs = dxfattribs or {} 541 542 # if layer is not deliberately set, set same layer as creator entity, 543 # at least VERTEX should have the same layer as the POLYGON entity. 544 # Don't know if that is also important for the ATTRIB & INSERT entity. 545 if 'layer' not in dxfattribs: 546 dxfattribs['layer'] = self.dxf.layer 547 if self.doc: 548 entity = factory.create_db_entry(type_, dxfattribs, self.doc) 549 else: 550 entity = factory.new(type_, dxfattribs) 551 entity.dxf.owner = self.dxf.owner 552 entity.dxf.paperspace = self.dxf.paperspace 553 return entity 554 555 556@factory.register_entity 557class SeqEnd(DXFGraphic): 558 DXFTYPE = 'SEQEND' 559 560 561def add_entity(entity: 'DXFGraphic', layout: 'BaseLayout') -> None: 562 """ Add `entity` entity to the entity database and to the given `layout`. 563 """ 564 assert entity.dxf.handle is None 565 assert layout is not None 566 if layout.doc: 567 factory.bind(entity, layout.doc) 568 layout.add_entity(entity) 569 570 571def replace_entity(source: 'DXFGraphic', target: 'DXFGraphic', 572 layout: 'BaseLayout') -> None: 573 """ Add `target` entity to the entity database and to the given `layout` 574 and replace the `source` entity by the `target` entity. 575 576 """ 577 assert target.dxf.handle is None 578 assert layout is not None 579 target.dxf.handle = source.dxf.handle 580 if source in layout: 581 layout.delete_entity(source) 582 if layout.doc: 583 factory.bind(target, layout.doc) 584 layout.add_entity(target) 585 else: 586 source.destroy() 587