1# Copyright (c) 2019-2020 Manfred Moitzi 2# License: MIT License 3from typing import ( 4 TYPE_CHECKING, Iterable, Union, List, cast, Tuple, Sequence, Dict, 5) 6from itertools import chain 7from ezdxf.lldxf import validator 8from ezdxf.lldxf.attributes import ( 9 DXFAttr, DXFAttributes, DefSubclass, XType, RETURN_DEFAULT, 10 group_code_mapping 11) 12from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, VERTEXNAMES 13from ezdxf.lldxf import const 14from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS 15from ezdxf.math.transformtools import OCSTransform, NonUniformScalingError 16from ezdxf.render.polyline import virtual_polyline_entities 17from ezdxf.explode import explode_entity 18from ezdxf.query import EntityQuery 19from ezdxf.entities import factory 20from ezdxf.audit import AuditError 21from .dxfentity import base_class, SubclassProcessor 22from .dxfgfx import DXFGraphic, acdb_entity 23from .lwpolyline import FORMAT_CODES 24from .subentity import LinkedEntities 25 26if TYPE_CHECKING: 27 from ezdxf.eztypes import ( 28 TagWriter, Vertex, FaceType, DXFNamespace, Line, Arc, Face3d, 29 BaseLayout, Auditor, 30 ) 31 32__all__ = ['Polyline', 'Polyface', 'Polymesh'] 33 34acdb_polyline = DefSubclass('AcDbPolylineDummy', { 35 # AcDbPolylineDummy is a temporary solution while loading 36 # Group code 66 is obsolete - Vertices follow flag 37 38 # Elevation is a "dummy" point. The x and y values are always 0, 39 # and the Z value is the polyline elevation: 40 'elevation': DXFAttr(10, xtype=XType.point3d, default=NULLVEC), 41 42 # Polyline flags (bit-coded): 43 # 1 = closed POLYLINE or a POLYMESH closed in the M direction 44 # 2 = Curve-fit vertices have been added 45 # 4 = Spline-fit vertices have been added 46 # 8 = 3D POLYLINE 47 # 16 = POLYMESH 48 # 32 = POLYMESH is closed in the N direction 49 # 64 = POLYFACE 50 # 128 = linetype pattern is generated continuously around the vertices 51 'flags': DXFAttr(70, default=0), 52 'default_start_width': DXFAttr(40, default=0, optional=True), 53 'default_end_width': DXFAttr(41, default=0, optional=True), 54 'm_count': DXFAttr( 55 71, default=0, optional=True, 56 validator=validator.is_greater_or_equal_zero, 57 fixer=RETURN_DEFAULT, 58 ), 59 'n_count': DXFAttr( 60 72, default=0, optional=True, 61 validator=validator.is_greater_or_equal_zero, 62 fixer=RETURN_DEFAULT, 63 ), 64 'm_smooth_density': DXFAttr(73, default=0, optional=True), 65 'n_smooth_density': DXFAttr(74, default=0, optional=True), 66 67 # Curves and smooth surface type: 68 # 0 = No smooth surface fitted 69 # 5 = Quadratic B-spline surface 70 # 6 = Cubic B-spline surface 71 # 8 = Bezier surface 72 'smooth_type': DXFAttr( 73 75, default=0, optional=True, 74 validator=validator.is_one_of({0, 5, 6, 8}), 75 fixer=RETURN_DEFAULT, 76 ), 77 'thickness': DXFAttr(39, default=0, optional=True), 78 'extrusion': DXFAttr( 79 210, xtype=XType.point3d, default=Z_AXIS, optional=True, 80 validator=validator.is_not_null_vector, 81 fixer=RETURN_DEFAULT, 82 ), 83}) 84acdb_polyline_group_codes = group_code_mapping(acdb_polyline, ignore=(66, )) 85 86# Notes to SEQEND: 87# todo: A loaded entity should have a valid SEQEND, a POLYLINE without vertices 88# makes no sense - has to be tested 89# 90# A virtual POLYLINE does not need a SEQEND, because it can not be exported, 91# therefore the SEQEND entity should not be created in the 92# DXFEntity.post_new_hook() method. 93# 94# A bounded POLYLINE needs a SEQEND to valid at export, therefore the 95# LinkedEntities.post_bind_hook() method creates a new SEQEND after binding 96# the entity to a document if needed. 97 98 99@factory.register_entity 100class Polyline(LinkedEntities): 101 """ DXF POLYLINE entity """ 102 DXFTYPE = 'POLYLINE' 103 DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_polyline) 104 # polyline flags (70) 105 CLOSED = 1 106 MESH_CLOSED_M_DIRECTION = CLOSED 107 CURVE_FIT_VERTICES_ADDED = 2 108 SPLINE_FIT_VERTICES_ADDED = 4 109 POLYLINE_3D = 8 110 POLYMESH = 16 111 MESH_CLOSED_N_DIRECTION = 32 112 POLYFACE = 64 113 GENERATE_LINETYPE_PATTERN = 128 114 # polymesh smooth type (75) 115 NO_SMOOTH = 0 116 QUADRATIC_BSPLINE = 5 117 CUBIC_BSPLINE = 6 118 BEZIER_SURFACE = 8 119 ANY3D = POLYLINE_3D | POLYMESH | POLYFACE 120 121 @property 122 def vertices(self): 123 return self._sub_entities 124 125 def load_dxf_attribs( 126 self, processor: SubclassProcessor = None) -> 'DXFNamespace': 127 dxf = super().load_dxf_attribs(processor) 128 if processor: 129 processor.fast_load_dxfattribs( 130 dxf, acdb_polyline_group_codes, subclass=2, recover=True) 131 return dxf 132 133 def export_dxf(self, tagwriter: 'TagWriter'): 134 """ Export POLYLINE entity and all linked entities: VERTEX, SEQEND. 135 """ 136 super().export_dxf(tagwriter) 137 # export sub-entities 138 self.process_sub_entities(lambda e: e.export_dxf(tagwriter)) 139 140 def export_entity(self, tagwriter: 'TagWriter') -> None: 141 """ Export POLYLINE specific data as DXF tags. """ 142 super().export_entity(tagwriter) 143 if tagwriter.dxfversion > DXF12: 144 tagwriter.write_tag2(SUBCLASS_MARKER, self.get_mode()) 145 146 tagwriter.write_tag2(66, 1) # Vertices follow 147 self.dxf.export_dxf_attribs(tagwriter, [ 148 'elevation', 'flags', 'default_start_width', 'default_end_width', 149 'm_count', 'n_count', 'm_smooth_density', 'n_smooth_density', 150 'smooth_type', 'thickness', 'extrusion', 151 ]) 152 153 def on_layer_change(self, layer: str): 154 """ Event handler for layer change. Changes also the layer of all vertices. 155 156 Args: 157 layer: new layer as string 158 159 """ 160 for v in self.vertices: 161 v.dxf.layer = layer 162 163 def on_linetype_change(self, linetype: str): 164 """ Event handler for linetype change. Changes also the linetype of all 165 vertices. 166 167 Args: 168 linetype: new linetype as string 169 170 """ 171 for v in self.vertices: 172 v.dxf.linetype = linetype 173 174 def get_vertex_flags(self) -> int: 175 return const.VERTEX_FLAGS[self.get_mode()] 176 177 def get_mode(self) -> str: 178 """ Returns POLYLINE type as string: 179 180 - 'AcDb2dPolyline' 181 - 'AcDb3dPolyline' 182 - 'AcDbPolygonMesh' 183 - 'AcDbPolyFaceMesh' 184 185 """ 186 if self.is_3d_polyline: 187 return 'AcDb3dPolyline' 188 elif self.is_polygon_mesh: 189 return 'AcDbPolygonMesh' 190 elif self.is_poly_face_mesh: 191 return 'AcDbPolyFaceMesh' 192 else: 193 return 'AcDb2dPolyline' 194 195 @property 196 def is_2d_polyline(self) -> bool: 197 """ ``True`` if POLYLINE is a 2D polyline. """ 198 return self.dxf.flags & self.ANY3D == 0 199 200 @property 201 def is_3d_polyline(self) -> bool: 202 """ ``True`` if POLYLINE is a 3D polyline. """ 203 return bool(self.dxf.flags & self.POLYLINE_3D) 204 205 @property 206 def is_polygon_mesh(self) -> bool: 207 """ ``True`` if POLYLINE is a polygon mesh, see :class:`Polymesh` """ 208 return bool(self.dxf.flags & self.POLYMESH) 209 210 @property 211 def is_poly_face_mesh(self) -> bool: 212 """ ``True`` if POLYLINE is a poly face mesh, see :class:`Polyface` """ 213 return bool(self.dxf.flags & self.POLYFACE) 214 215 @property 216 def is_closed(self) -> bool: 217 """ ``True`` if POLYLINE is closed. """ 218 return bool(self.dxf.flags & self.CLOSED) 219 220 @property 221 def is_m_closed(self) -> bool: 222 """ ``True`` if POLYLINE (as :class:`Polymesh`) is closed in m 223 direction. 224 """ 225 return bool(self.dxf.flags & self.MESH_CLOSED_M_DIRECTION) 226 227 @property 228 def is_n_closed(self) -> bool: 229 """ ``True`` if POLYLINE (as :class:`Polymesh`) is closed in n 230 direction. 231 """ 232 return bool(self.dxf.flags & self.MESH_CLOSED_N_DIRECTION) 233 234 @property 235 def has_arc(self) -> bool: 236 """ Returns ``True`` if 2D POLYLINE has an arc segment. """ 237 if self.is_2d_polyline: 238 return any( 239 v.dxf.hasattr('bulge') and bool(v.dxf.bulge) for v in 240 self.vertices 241 ) 242 else: 243 return False 244 245 @property 246 def has_width(self) -> bool: 247 """ Returns ``True`` if 2D POLYLINE has default width values or any 248 segment with width attributes. 249 250 .. versionadded:: 0.14 251 252 """ 253 if self.is_2d_polyline: 254 if self.dxf.hasattr('default_start_width') and bool( 255 self.dxf.default_start_width): 256 return True 257 if self.dxf.hasattr('default_end_width') and bool( 258 self.dxf.default_end_width): 259 return True 260 for v in self.vertices: 261 if v.dxf.hasattr('start_width') and bool(v.dxf.start_width): 262 return True 263 if v.dxf.hasattr('end_width') and bool(v.dxf.end_width): 264 return True 265 return False 266 267 def m_close(self, status=True) -> None: 268 """ Close POLYMESH in m direction if `status` is ``True`` (also closes 269 POLYLINE), clears closed state if `status` is ``False``. 270 """ 271 self.set_flag_state(self.MESH_CLOSED_M_DIRECTION, status, name='flags') 272 273 def n_close(self, status=True) -> None: 274 """ Close POLYMESH in n direction if `status` is ``True``, clears closed 275 state if `status` is ``False``. 276 """ 277 self.set_flag_state(self.MESH_CLOSED_N_DIRECTION, status, name='flags') 278 279 def close(self, m_close=True, n_close=False) -> None: 280 """ Set closed state of POLYMESH and POLYLINE in m direction and n 281 direction. ``True`` set closed flag, ``False`` clears closed flag. 282 """ 283 self.m_close(m_close) 284 self.n_close(n_close) 285 286 def __len__(self) -> int: 287 """ Returns count of :class:`Vertex` entities. """ 288 return len(self.vertices) 289 290 def __getitem__(self, pos) -> 'DXFVertex': 291 """ Get :class:`Vertex` entity at position `pos`, supports ``list`` 292 slicing. 293 """ 294 return self.vertices[pos] 295 296 def points(self) -> Iterable[Vec3]: 297 """ Returns iterable of all polyline vertices as ``(x, y, z)`` tuples, 298 not as :class:`Vertex` objects. 299 """ 300 return (vertex.dxf.location for vertex in self.vertices) 301 302 def _append_vertex(self, vertex: 'DXFVertex') -> None: 303 self.vertices.append(vertex) 304 305 def append_vertices(self, points: Iterable['Vertex'], 306 dxfattribs: Dict = None) -> None: 307 """ Append multiple :class:`Vertex` entities at location `points`. 308 309 Args: 310 points: iterable of ``(x, y[, z])`` tuples 311 dxfattribs: dict of DXF attributes for :class:`Vertex` class 312 313 """ 314 dxfattribs = dxfattribs or {} 315 for vertex in self._build_dxf_vertices(points, dxfattribs): 316 self._append_vertex(vertex) 317 318 def append_formatted_vertices(self, points: Iterable['Vertex'], 319 format: str = 'xy', 320 dxfattribs: Dict = None) -> None: 321 """ Append multiple :class:`Vertex` entities at location `points`. 322 323 Args: 324 points: iterable of (x, y, [start_width, [end_width, [bulge]]]) 325 tuple 326 format: format string, default is ``'xy'``, see: :ref:`format codes` 327 dxfattribs: dict of DXF attributes for :class:`Vertex` class 328 329 """ 330 dxfattribs = dxfattribs or {} 331 dxfattribs['flags'] = ( 332 dxfattribs.get('flags', 0) | self.get_vertex_flags() 333 ) 334 335 # same DXF attributes for VERTEX entities as for POLYLINE 336 dxfattribs['owner'] = self.dxf.owner 337 dxfattribs['layer'] = self.dxf.layer 338 if self.dxf.hasattr('linetype'): 339 dxfattribs['linetype'] = self.dxf.linetype 340 341 for point in points: 342 attribs = vertex_attribs(point, format) 343 attribs.update(dxfattribs) 344 vertex = self._new_compound_entity('VERTEX', attribs) 345 self._append_vertex(vertex) 346 347 def append_vertex(self, point: 'Vertex', dxfattribs: dict = None) -> None: 348 """ Append a single :class:`Vertex` entity at location `point`. 349 350 Args: 351 point: as ``(x, y[, z])`` tuple 352 dxfattribs: dict of DXF attributes for :class:`Vertex` class 353 354 """ 355 dxfattribs = dxfattribs or {} 356 for vertex in self._build_dxf_vertices([point], dxfattribs): 357 self._append_vertex(vertex) 358 359 def insert_vertices(self, pos: int, points: Iterable['Vertex'], 360 dxfattribs: dict = None) -> None: 361 """ 362 Insert vertices `points` into :attr:`Polyline.vertices` list 363 at insertion location `pos` . 364 365 Args: 366 pos: insertion position of list :attr:`Polyline.vertices` 367 points: list of ``(x, y[, z])`` tuples 368 dxfattribs: dict of DXF attributes for :class:`Vertex` class 369 370 """ 371 dxfattribs = dxfattribs or {} 372 self.vertices[pos:pos] = list( 373 self._build_dxf_vertices(points, dxfattribs)) 374 375 def _build_dxf_vertices(self, points: Iterable['Vertex'], 376 dxfattribs: dict) -> List['DXFVertex']: 377 """ Converts point (x, y, z)-tuples into DXFVertex objects. 378 379 Args: 380 points: list of (x, y, z)-tuples 381 dxfattribs: dict of DXF attributes 382 """ 383 dxfattribs['flags'] = ( 384 dxfattribs.get('flags', 0) | self.get_vertex_flags() 385 ) 386 387 # same DXF attributes for VERTEX entities as for POLYLINE 388 dxfattribs['owner'] = self.dxf.owner 389 dxfattribs['layer'] = self.dxf.layer 390 if self.dxf.hasattr('linetype'): 391 dxfattribs['linetype'] = self.dxf.linetype 392 for point in points: 393 dxfattribs['location'] = Vec3(point) 394 yield self._new_compound_entity('VERTEX', dxfattribs) 395 396 def cast(self) -> Union['Polyline', 'Polymesh', 'Polyface']: 397 mode = self.get_mode() 398 if mode == 'AcDbPolyFaceMesh': 399 return Polyface.from_polyline(self) 400 elif mode == 'AcDbPolygonMesh': 401 return Polymesh.from_polyline(self) 402 else: 403 return self 404 405 def transform(self, m: Matrix44) -> 'Polyline': 406 """ Transform the POLYLINE entity by transformation matrix `m` inplace. 407 """ 408 409 def _ocs_locations(elevation): 410 for vertex in self.vertices: 411 location = vertex.dxf.location 412 if elevation is not None: 413 # Older DXF version may not have written the z-axis, which 414 # is now 0 by default in ezdxf, so replace existing z-axis 415 # by elevation value. 416 location = location.replace(z=elevation) 417 yield location 418 419 if self.is_2d_polyline: 420 dxf = self.dxf 421 ocs = OCSTransform(self.dxf.extrusion, m) 422 if not ocs.scale_uniform and self.has_arc: 423 # Parent function has to catch this Exception and explode this 424 # 2D POLYLINE into LINE and ELLIPSE entities. 425 raise NonUniformScalingError( 426 '2D POLYLINE with arcs does not support non uniform scaling' 427 ) 428 429 if dxf.hasattr('elevation'): 430 z_axis = dxf.elevation.z 431 else: 432 z_axis = None 433 434 vertices = [ 435 ocs.transform_vertex(vertex) for vertex in 436 _ocs_locations(z_axis) 437 ] 438 439 # All vertices of a 2D polyline have the same z-axis: 440 if vertices: 441 dxf.elevation = vertices[0].replace(x=0, y=0) 442 443 for vertex, location in zip(self.vertices, vertices): 444 vertex.dxf.location = location 445 446 if dxf.hasattr('thickness'): 447 dxf.thickness = ocs.transform_length((0, 0, dxf.thickness)) 448 449 dxf.extrusion = ocs.new_extrusion 450 else: 451 for vertex in self.vertices: 452 vertex.transform(m) 453 return self 454 455 def explode(self, target_layout: 'BaseLayout' = None) -> 'EntityQuery': 456 """ Explode POLYLINE as DXF LINE, ARC or 3DFACE primitives into target 457 layout, if the target layout is ``None``, the target layout is the 458 layout of the POLYLINE entity . 459 Returns an :class:`~ezdxf.query.EntityQuery` container including all 460 DXF primitives. 461 462 Args: 463 target_layout: target layout for DXF primitives, ``None`` for same 464 layout as source entity. 465 466 """ 467 return explode_entity(self, target_layout) 468 469 def virtual_entities(self) -> Iterable[Union['Line', 'Arc', 'Face3d']]: 470 """ Yields 'virtual' parts of POLYLINE as LINE, ARC or 3DFACE 471 primitives. 472 473 This entities are located at the original positions, but are not stored 474 in the entity database, have no handle and are not assigned to any 475 layout. 476 477 """ 478 return virtual_polyline_entities(self) 479 480 def audit(self, auditor: 'Auditor') -> None: 481 """ Audit and repair POLYLINE entity. """ 482 483 def audit_sub_entity(entity): 484 entity.doc = doc # grant same document 485 dxf = entity.dxf 486 if dxf.owner != owner: 487 dxf.owner = owner 488 if dxf.layer != layer: 489 dxf.layer = layer 490 491 doc = self.doc 492 owner = self.dxf.handle 493 layer = self.dxf.layer 494 for vertex in self.vertices: 495 audit_sub_entity(vertex) 496 497 seqend = self.seqend 498 if seqend: 499 audit_sub_entity(seqend) 500 elif doc: 501 self.new_seqend() 502 auditor.fixed_error( 503 code=AuditError.MISSING_REQUIRED_SEQEND, 504 message=f'Create required SEQEND entity for {str(self)}.', 505 dxf_entity=self, 506 ) 507 508 509class Polyface(Polyline): 510 """ 511 PolyFace structure: 512 513 POLYLINE 514 AcDbEntity 515 AcDbPolyFaceMesh 516 VERTEX - Vertex 517 AcDbEntity 518 AcDbVertex 519 AcDbPolyFaceMeshVertex 520 VERTEX - Face 521 AcDbEntity 522 AcDbFaceRecord 523 SEQEND 524 525 Order of mesh_vertices and face_records is important (DXF R2010): 526 527 1. mesh_vertices: the polyface mesh vertex locations 528 2. face_records: indices of the face forming vertices 529 530 """ 531 532 @classmethod 533 def from_polyline(cls, polyline: Polyline) -> 'Polyface': 534 polyface = cls.shallow_copy(polyline) 535 polyface._sub_entities = polyline._sub_entities 536 polyface.seqend = polyline.seqend 537 # do not destroy polyline - all data would be lost 538 return polyface 539 540 def append_face(self, face: 'FaceType', dxfattribs: Dict = None) -> None: 541 """ 542 Append a single face. A `face` is a list of ``(x, y, z)`` tuples. 543 544 Args: 545 face: List[``(x, y, z)`` tuples] 546 dxfattribs: dict of DXF attributes for :class:`Vertex` entity 547 548 """ 549 self.append_faces([face], dxfattribs) 550 551 def _points_to_dxf_vertices(self, points: Iterable['Vertex'], 552 dxfattribs: Dict) -> List['DXFVertex']: 553 """ Converts point (x,y, z)-tuples into DXFVertex objects. 554 555 Args: 556 points: List[``(x, y, z)`` tuples] 557 dxfattribs: dict of DXF attributes for :class:`Vertex` entity 558 559 """ 560 dxfattribs['flags'] = ( 561 dxfattribs.get('flags', 0) | self.get_vertex_flags() 562 ) 563 564 # All vertices have to be on the same layer as the POLYLINE entity: 565 dxfattribs['layer'] = self.get_dxf_attrib('layer', '0') 566 vertices: List[DXFVertex] = [] 567 for point in points: 568 dxfattribs['location'] = point 569 vertices.append(cast( 570 'DXFVertex', 571 self._new_compound_entity('VERTEX', dxfattribs) 572 )) 573 return vertices 574 575 def append_faces(self, faces: Iterable['FaceType'], 576 dxfattribs: Dict = None) -> None: 577 """ 578 Append multiple `faces`. `faces` is a list of single faces and a single 579 face is a list of ``(x, y, z)`` tuples. 580 581 Args: 582 faces: list of List[``(x, y, z)`` tuples] 583 dxfattribs: dict of DXF attributes for :class:`Vertex` entity 584 585 """ 586 587 def new_face_record() -> 'DXFVertex': 588 dxfattribs['flags'] = const.VTX_3D_POLYFACE_MESH_VERTEX 589 # location of face record vertex is always (0, 0, 0) 590 dxfattribs['location'] = Vec3() 591 return self._new_compound_entity('VERTEX', dxfattribs) 592 593 dxfattribs = dxfattribs or {} 594 595 existing_vertices, existing_faces = self.indexed_faces() 596 new_faces: List[FaceProxy] = [] 597 for face in faces: 598 face_mesh_vertices = self._points_to_dxf_vertices(face, {}) 599 # Index of first new vertex 600 index = len(existing_vertices) 601 existing_vertices.extend(face_mesh_vertices) 602 face_record = FaceProxy(new_face_record(), existing_vertices) 603 604 # Set VERTEX indices: 605 face_record.indices = tuple( 606 range(index, index + len(face_mesh_vertices)) 607 ) 608 new_faces.append(face_record) 609 self._rebuild(chain(existing_faces, new_faces)) 610 611 def _rebuild(self, faces: Iterable['FaceProxy'], 612 precision: int = 6) -> None: 613 """ 614 Build a valid POLYFACE structure from `faces`. 615 616 Args: 617 faces: iterable of FaceProxy objects. 618 619 """ 620 polyface_builder = PolyfaceBuilder(faces, precision=precision) 621 self._sub_entities = [] 622 self._sub_entities = polyface_builder.get_vertices() 623 self.update_count(polyface_builder.nvertices, polyface_builder.nfaces) 624 625 def update_count(self, nvertices: int, nfaces: int) -> None: 626 self.dxf.m_count = nvertices 627 self.dxf.n_count = nfaces 628 629 def optimize(self, precision: int = 6) -> None: 630 """ 631 Rebuilds :class:`Polyface` including vertex optimization by merging 632 vertices with nearly same vertex locations. 633 634 Args: 635 precision: floating point precision for determining identical 636 vertex locations 637 638 """ 639 vertices, faces = self.indexed_faces() 640 self._rebuild(faces, precision) 641 642 def faces(self) -> Iterable[List['DXFVertex']]: 643 """ 644 Iterable of all faces, a face is a tuple of vertices. 645 646 Returns: 647 list: [vertex, vertex, vertex, [vertex,] face_record] 648 649 """ 650 _, faces = self.indexed_faces() 651 for face in faces: 652 face_vertices = list(face) 653 face_vertices.append(face.face_record) 654 yield face_vertices 655 656 def indexed_faces(self) -> Tuple[List['DXFVertex'], Iterable['FaceProxy']]: 657 """ 658 Returns a list of all vertices and a generator of FaceProxy() objects. 659 660 (internal API) 661 """ 662 vertices = [] 663 face_records = [] 664 for vertex in self.vertices: # type: DXFVertex 665 ( 666 vertices if vertex.is_poly_face_mesh_vertex else face_records).append( 667 vertex) 668 669 faces = (FaceProxy(face_record, vertices) for face_record in 670 face_records) 671 return vertices, faces 672 673 674class FaceProxy: 675 """ 676 Represents a single face of a polyface structure. (internal class) 677 678 vertices: 679 680 List of all polyface vertices. 681 682 face_record: 683 684 The face forming vertex of type ``AcDbFaceRecord``, contains the indices 685 to the face building vertices. Indices of the DXF structure are 1-based 686 and a negative index indicates the beginning of an invisible edge. 687 Face.face_record.dxf.color determines the color of the face. 688 689 indices: 690 691 Indices to the face building vertices as tuple. This indices are 0-base 692 and are used to get vertices from the list `Face.vertices`. 693 694 """ 695 __slots__ = ('vertices', 'face_record', 'indices') 696 697 def __init__(self, face_record: 'DXFVertex', 698 vertices: Sequence['DXFVertex']): 699 """ Returns iterable of all face vertices as :class:`Vertex` entities. 700 """ 701 self.vertices: Sequence[DXFVertex] = vertices 702 self.face_record: DXFVertex = face_record 703 self.indices: Sequence[int] = self._indices() 704 705 def __len__(self) -> int: 706 """ Returns count of face vertices (without face_record). """ 707 return len(self.indices) 708 709 def __getitem__(self, pos: int) -> 'DXFVertex': 710 """ Returns :class:`Vertex` at position `pos`. 711 712 Args: 713 pos: vertex position 0-based 714 715 """ 716 return self.vertices[self.indices[pos]] 717 718 def __iter__(self) -> Iterable['DXFVertex']: 719 return (self.vertices[index] for index in self.indices) 720 721 def points(self) -> Iterable['Vertex']: 722 """ Returns iterable of all face vertex locations as (x, y, z)-tuples. 723 """ 724 return (vertex.dxf.location for vertex in self) 725 726 def _raw_indices(self) -> Iterable[int]: 727 return (self.face_record.get_dxf_attrib(name, 0) for name in 728 const.VERTEXNAMES) 729 730 def _indices(self) -> Sequence[int]: 731 return tuple( 732 abs(index) - 1 for index in self._raw_indices() if index != 0) 733 734 def is_edge_visible(self, pos: int) -> bool: 735 """ Returns ``True`` if edge starting at vertex `pos` is visible. 736 737 Args: 738 pos: vertex position 0-based 739 740 """ 741 name = const.VERTEXNAMES[pos] 742 return self.face_record.get_dxf_attrib(name) > 0 743 744 745class PolyfaceBuilder: 746 """ Optimized POLYFACE builder. (internal class) """ 747 748 def __init__(self, faces: Iterable['FaceProxy'], precision: int = 6): 749 self.precision: int = precision 750 self.faces: List[DXFVertex] = [] 751 self.vertices: List[DXFVertex] = [] 752 self.index_mapping: Dict[Tuple[float, ...], int] = {} 753 self.build(faces) 754 755 @property 756 def nvertices(self) -> int: 757 return len(self.vertices) 758 759 @property 760 def nfaces(self) -> int: 761 return len(self.faces) 762 763 def get_vertices(self) -> List['DXFVertex']: 764 vertices = self.vertices[:] 765 vertices.extend(self.faces) 766 return vertices 767 768 def build(self, faces: Iterable['FaceProxy']) -> None: 769 for face in faces: 770 face_record = face.face_record 771 for vertex, name in zip(face, VERTEXNAMES): 772 index = self.add(vertex) 773 # preserve sign of old index value 774 sign = -1 if face_record.dxf.get(name, 0) < 0 else +1 775 face_record.dxf.set(name, (index + 1) * sign) 776 self.faces.append(face_record) 777 778 def add(self, vertex: 'DXFVertex') -> int: 779 def key(point): 780 return tuple((round(coord, self.precision) for coord in point)) 781 782 location = key(vertex.dxf.location) 783 try: 784 return self.index_mapping[location] 785 except KeyError: 786 index = len(self.vertices) 787 self.index_mapping[location] = index 788 self.vertices.append(vertex) 789 return index 790 791 792class Polymesh(Polyline): 793 """ 794 PolyMesh structure: 795 796 POLYLINE 797 AcDbEntity 798 AcDbPolygonMesh 799 VERTEX 800 AcDbEntity 801 AcDbVertex 802 AcDbPolygonMeshVertex 803 """ 804 805 @classmethod 806 def from_polyline(cls, polyline: Polyline) -> 'Polymesh': 807 polymesh = cls.shallow_copy(polyline) 808 polymesh._sub_entities = polyline._sub_entities 809 polymesh.seqend = polyline.seqend 810 return polymesh 811 812 def set_mesh_vertex(self, pos: Tuple[int, int], point: 'Vertex', 813 dxfattribs: dict = None): 814 """ 815 Set location and DXF attributes of a single mesh vertex. 816 817 Args: 818 pos: 0-based (row, col)-tuple, position of mesh vertex 819 point: (x, y, z)-tuple, new 3D coordinates of the mesh vertex 820 dxfattribs: dict of DXF attributes 821 822 """ 823 dxfattribs = dxfattribs or {} 824 dxfattribs['location'] = point 825 vertex = self.get_mesh_vertex(pos) 826 vertex.update_dxf_attribs(dxfattribs) 827 828 def get_mesh_vertex(self, pos: Tuple[int, int]) -> 'DXFVertex': 829 """ 830 Get location of a single mesh vertex. 831 832 Args: 833 pos: 0-based ``(row, col)`` tuple, position of mesh vertex 834 835 """ 836 m_count = self.dxf.m_count 837 n_count = self.dxf.n_count 838 m, n = pos 839 if 0 <= m < m_count and 0 <= n < n_count: 840 pos = m * n_count + n 841 return self.vertices[pos] 842 else: 843 raise const.DXFIndexError(repr(pos)) 844 845 def get_mesh_vertex_cache(self) -> 'MeshVertexCache': 846 """ 847 Get a :class:`MeshVertexCache` object for this POLYMESH. 848 The caching object provides fast access to the :attr:`location` 849 attribute of mesh vertices. 850 851 """ 852 return MeshVertexCache(self) 853 854 855class MeshVertexCache: 856 """ Cache mesh vertices in a dict, keys are 0-based (row, col)-tuples. 857 858 vertices: 859 Dict of mesh vertices, keys are 0-based (row, col)-tuples. Writing to 860 this dict doesn't change the DXF entity. 861 862 """ 863 __slots__ = ('vertices',) 864 865 def __init__(self, mesh: 'Polyline'): 866 self.vertices: Dict[Tuple[int, int], DXFVertex] = self._setup( 867 mesh, mesh.dxf.m_count, mesh.dxf.n_count 868 ) 869 870 def _setup(self, mesh: 'Polyline', m_count: int, n_count: int) -> dict: 871 cache: Dict[Tuple[int, int], DXFVertex] = {} 872 vertices = iter(mesh.vertices) 873 for m in range(m_count): 874 for n in range(n_count): 875 cache[(m, n)] = next(vertices) 876 return cache 877 878 def __getitem__(self, pos: Tuple[int, int]) -> 'Vertex': 879 """ 880 Get mesh vertex location as (x, y, z)-tuple. 881 882 Args: 883 pos: 0-based (row, col)-tuple. 884 885 """ 886 try: 887 return self.vertices[pos].dxf.location 888 except KeyError: 889 raise const.DXFIndexError(repr(pos)) 890 891 def __setitem__(self, pos: Tuple[int, int], location: 'Vertex') -> None: 892 """ 893 Get mesh vertex location as (x, y, z)-tuple. 894 895 Args: 896 pos: 0-based (row, col)-tuple. 897 location: (x, y, z)-tuple 898 899 """ 900 try: 901 self.vertices[pos].dxf.location = location 902 except KeyError: 903 raise const.DXFIndexError(repr(pos)) 904 905 906acdb_vertex = DefSubclass('AcDbVertex', { # last subclass index -1 907 # Location point in OCS if 2D, and WCS if 3D 908 'location': DXFAttr(10, xtype=XType.point3d), 909 'start_width': DXFAttr(40, default=0, optional=True), 910 'end_width': DXFAttr(41, default=0, optional=True), 911 912 # Bulge (optional; default is 0). The bulge is the tangent of one fourth 913 # the included angle for an arc segment, made negative if the arc goes 914 # clockwise from the start point to the endpoint. A bulge of 0 indicates 915 # a straight segment, and a bulge of 1 is a semicircle. 916 'bulge': DXFAttr(42, default=0, optional=True), 917 'flags': DXFAttr(70, default=0), 918 # Curve fit tangent direction (in degrees) 919 'tangent': DXFAttr(50, optional=True), 920 'vtx0': DXFAttr(71, optional=True), 921 'vtx1': DXFAttr(72, optional=True), 922 'vtx2': DXFAttr(73, optional=True), 923 'vtx3': DXFAttr(74, optional=True), 924 'vertex_identifier': DXFAttr(91, optional=True), 925}) 926acdb_vertex_group_codes = group_code_mapping(acdb_vertex) 927 928 929@factory.register_entity 930class DXFVertex(DXFGraphic): 931 """ DXF VERTEX entity """ 932 DXFTYPE = 'VERTEX' 933 934 DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_vertex) 935 # Extra vertex created by curve-fitting: 936 EXTRA_VERTEX_CREATED = 1 937 938 # Curve-fit tangent defined for this vertex. A curve-fit tangent direction 939 # of 0 may be omitted from the DXF output, but is significant if this bit 940 # is set: 941 CURVE_FIT_TANGENT = 2 942 943 # 4 = unused, never set in dxf files 944 # Spline vertex created by spline-fitting 945 SPLINE_VERTEX_CREATED = 8 946 SPLINE_FRAME_CONTROL_POINT = 16 947 POLYLINE_3D_VERTEX = 32 948 POLYGON_MESH_VERTEX = 64 949 POLYFACE_MESH_VERTEX = 128 950 FACE_FLAGS = POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX 951 VTX3D = POLYLINE_3D_VERTEX + POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX 952 953 def load_dxf_attribs( 954 self, processor: SubclassProcessor = None) -> 'DXFNamespace': 955 dxf = super().load_dxf_attribs(processor) 956 if processor: 957 # VERTEX can have 3 subclasses if representing a `face record` or 958 # 4 subclasses if representing a vertex location, just the last 959 # subclass contains data: 960 processor.fast_load_dxfattribs( 961 dxf, acdb_vertex_group_codes, subclass=-1, recover=True) 962 return dxf 963 964 def export_entity(self, tagwriter: 'TagWriter') -> None: 965 """ Export entity specific data as DXF tags. """ 966 super().export_entity(tagwriter) 967 if tagwriter.dxfversion > DXF12: 968 if self.is_face_record: 969 tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbFaceRecord') 970 else: 971 tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDbVertex') 972 if self.is_3d_polyline_vertex: 973 tagwriter.write_tag2( 974 SUBCLASS_MARKER, 'AcDb3dPolylineVertex' 975 ) 976 elif self.is_poly_face_mesh_vertex: 977 tagwriter.write_tag2( 978 SUBCLASS_MARKER, 'AcDbPolyFaceMeshVertex' 979 ) 980 elif self.is_polygon_mesh_vertex: 981 tagwriter.write_tag2( 982 SUBCLASS_MARKER, 'AcDbPolygonMeshVertex' 983 ) 984 else: 985 tagwriter.write_tag2(SUBCLASS_MARKER, 'AcDb2dVertex') 986 987 self.dxf.export_dxf_attribs(tagwriter, [ 988 'location', 'start_width', 'end_width', 'bulge', 'flags', 'tangent', 989 'vtx0', 'vtx1', 'vtx2', 'vtx3', 'vertex_identifier' 990 ]) 991 992 @property 993 def is_2d_polyline_vertex(self) -> bool: 994 return self.dxf.flags & self.VTX3D == 0 995 996 @property 997 def is_3d_polyline_vertex(self) -> bool: 998 return self.dxf.flags & self.POLYLINE_3D_VERTEX 999 1000 @property 1001 def is_polygon_mesh_vertex(self) -> bool: 1002 return self.dxf.flags & self.POLYGON_MESH_VERTEX 1003 1004 @property 1005 def is_poly_face_mesh_vertex(self) -> bool: 1006 return self.dxf.flags & self.FACE_FLAGS == self.FACE_FLAGS 1007 1008 @property 1009 def is_face_record(self) -> bool: 1010 return (self.dxf.flags & self.FACE_FLAGS) == self.POLYFACE_MESH_VERTEX 1011 1012 def transform(self, m: 'Matrix44') -> 'DXFVertex': 1013 """ Transform the VERTEX entity by transformation matrix `m` inplace. 1014 """ 1015 if self.is_face_record: 1016 return self 1017 self.dxf.location = m.transform(self.dxf.location) 1018 return self 1019 1020 def format(self, format='xyz') -> Sequence: 1021 """ Return formatted vertex components as tuple. 1022 1023 Format codes: 1024 1025 - ``x`` = x-coordinate 1026 - ``y`` = y-coordinate 1027 - ``z`` = z-coordinate 1028 - ``s`` = start width 1029 - ``e`` = end width 1030 - ``b`` = bulge value 1031 - ``v`` = (x, y, z) as tuple 1032 1033 Args: 1034 format: format string, default is "xyz" 1035 1036 .. versionadded:: 0.14 1037 1038 """ 1039 dxf = self.dxf 1040 v = Vec3(dxf.location) 1041 x, y, z = v.xyz 1042 b = dxf.bulge 1043 s = dxf.start_width 1044 e = dxf.end_width 1045 vars = locals() 1046 return tuple(vars[code] for code in format.lower()) 1047 1048 1049def vertex_attribs(data: Sequence[float], format='xyseb') -> dict: 1050 """ 1051 Create VERTEX attributes from input data. 1052 1053 Format codes: 1054 1055 - ``x`` = x-coordinate 1056 - ``y`` = y-coordinate 1057 - ``s`` = start width 1058 - ``e`` = end width 1059 - ``b`` = bulge value 1060 - ``v`` = (x, y [,z]) tuple (z-axis is ignored) 1061 1062 Args: 1063 data: list or tuple of point components 1064 format: format string, default is 'xyseb' 1065 1066 Returns: 1067 dict with keys: 'location', 'bulge', 'start_width', 'end_width' 1068 1069 """ 1070 attribs = dict() 1071 format = [code for code in format.lower() if code in FORMAT_CODES] 1072 location = Vec3() 1073 for code, value in zip(format, data): 1074 if code not in FORMAT_CODES: 1075 continue 1076 if code == 'v': 1077 location = Vec3(value) 1078 elif code == 'b': 1079 attribs['bulge'] = value 1080 elif code == 's': 1081 attribs['start_width'] = value 1082 elif code == 'e': 1083 attribs['end_width'] = value 1084 elif code == 'x': 1085 location = location.replace(x=value) 1086 elif code == 'y': 1087 location = location.replace(y=value) 1088 attribs['location'] = location 1089 return attribs 1090