Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/entities/polyline.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

1140 lines
38 KiB
Python

# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Iterable,
Iterator,
Union,
cast,
Sequence,
Optional,
)
from itertools import chain
from ezdxf.lldxf import validator
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
merge_group_code_mappings,
)
from ezdxf.lldxf.const import DXF12, SUBCLASS_MARKER, VERTEXNAMES
from ezdxf.lldxf import const
from ezdxf.math import Vec3, Matrix44, NULLVEC, Z_AXIS, UVec
from ezdxf.math.transformtools import OCSTransform, NonUniformScalingError
from ezdxf.render.polyline import virtual_polyline_entities
from ezdxf.explode import explode_entity
from ezdxf.query import EntityQuery
from ezdxf.entities import factory
from ezdxf.audit import AuditError
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import DXFGraphic, acdb_entity, acdb_entity_group_codes
from .lwpolyline import FORMAT_CODES
from .subentity import LinkedEntities
if TYPE_CHECKING:
from ezdxf.audit import Auditor
from ezdxf.entities import DXFNamespace, Line, Arc, Face3d
from ezdxf.layouts import BaseLayout
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.eztypes import FaceType
__all__ = ["Polyline", "Polyface", "Polymesh"]
acdb_polyline = DefSubclass(
"AcDbPolylineDummy",
{
# AcDbPolylineDummy is a temporary solution while loading
# Group code 66 is obsolete - Vertices follow flag
# Elevation is a "dummy" point. The x and y values are always 0,
# and the Z value is the polyline elevation:
"elevation": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# Polyline flags (bit-coded):
# 1 = closed POLYLINE or a POLYMESH closed in the M direction
# 2 = Curve-fit vertices have been added
# 4 = Spline-fit vertices have been added
# 8 = 3D POLYLINE
# 16 = POLYMESH
# 32 = POLYMESH is closed in the N direction
# 64 = POLYFACE
# 128 = linetype pattern is generated continuously around the vertices
"flags": DXFAttr(70, default=0),
"default_start_width": DXFAttr(40, default=0, optional=True),
"default_end_width": DXFAttr(41, default=0, optional=True),
"m_count": DXFAttr(
71,
default=0,
optional=True,
validator=validator.is_greater_or_equal_zero,
fixer=RETURN_DEFAULT,
),
"n_count": DXFAttr(
72,
default=0,
optional=True,
validator=validator.is_greater_or_equal_zero,
fixer=RETURN_DEFAULT,
),
"m_smooth_density": DXFAttr(73, default=0, optional=True),
"n_smooth_density": DXFAttr(74, default=0, optional=True),
# Curves and smooth surface type:
# 0 = No smooth surface fitted
# 5 = Quadratic B-spline surface
# 6 = Cubic B-spline surface
# 8 = Bezier surface
"smooth_type": DXFAttr(
75,
default=0,
optional=True,
validator=validator.is_one_of({0, 5, 6, 8}),
fixer=RETURN_DEFAULT,
),
"thickness": DXFAttr(39, default=0, optional=True),
"extrusion": DXFAttr(
210,
xtype=XType.point3d,
default=Z_AXIS,
optional=True,
validator=validator.is_not_null_vector,
fixer=RETURN_DEFAULT,
),
},
)
acdb_polyline_group_codes = group_code_mapping(acdb_polyline, ignore=(66,))
merged_polyline_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_polyline_group_codes # type: ignore
)
# Notes to SEQEND:
# todo: A loaded entity should have a valid SEQEND, a POLYLINE without vertices
# makes no sense - has to be tested
#
# A virtual POLYLINE does not need a SEQEND, because it can not be exported,
# therefore the SEQEND entity should not be created in the
# DXFEntity.post_new_hook() method.
#
# A bounded POLYLINE needs a SEQEND to valid at export, therefore the
# LinkedEntities.post_bind_hook() method creates a new SEQEND after binding
# the entity to a document if needed.
@factory.register_entity
class Polyline(LinkedEntities):
"""DXF POLYLINE entity
The POLYLINE entity is hard owner of its VERTEX entities and the SEQEND entity:
VERTEX.dxf.owner == POLYLINE.dxf.handle
SEQEND.dxf.owner == POLYLINE.dxf.handle
"""
DXFTYPE = "POLYLINE"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_polyline)
# polyline flags (70)
CLOSED = 1
MESH_CLOSED_M_DIRECTION = CLOSED
CURVE_FIT_VERTICES_ADDED = 2
SPLINE_FIT_VERTICES_ADDED = 4
POLYLINE_3D = 8
POLYMESH = 16
MESH_CLOSED_N_DIRECTION = 32
POLYFACE = 64
GENERATE_LINETYPE_PATTERN = 128
# polymesh smooth type (75)
NO_SMOOTH = 0
QUADRATIC_BSPLINE = 5
CUBIC_BSPLINE = 6
BEZIER_SURFACE = 8
ANY3D = POLYLINE_3D | POLYMESH | POLYFACE
@property
def vertices(self) -> list[DXFVertex]:
return self._sub_entities # type: ignore
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
# bypass DXFGraphic, loading proxy graphic is skipped!
dxf = super(DXFGraphic, self).load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, merged_polyline_group_codes)
return dxf
def export_dxf(self, tagwriter: AbstractTagWriter):
"""Export POLYLINE entity and all linked entities: VERTEX, SEQEND."""
super().export_dxf(tagwriter)
# export sub-entities
self.process_sub_entities(lambda e: e.export_dxf(tagwriter))
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export POLYLINE specific data as DXF tags."""
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, self.get_mode())
tagwriter.write_tag2(66, 1) # Vertices follow
self.dxf.export_dxf_attribs(
tagwriter,
[
"elevation",
"flags",
"default_start_width",
"default_end_width",
"m_count",
"n_count",
"m_smooth_density",
"n_smooth_density",
"smooth_type",
"thickness",
"extrusion",
],
)
def on_layer_change(self, layer: str):
"""Event handler for layer change. Changes also the layer of all vertices.
Args:
layer: new layer as string
"""
for v in self.vertices:
v.dxf.layer = layer
def on_linetype_change(self, linetype: str):
"""Event handler for linetype change. Changes also the linetype of all
vertices.
Args:
linetype: new linetype as string
"""
for v in self.vertices:
v.dxf.linetype = linetype
def get_vertex_flags(self) -> int:
return const.VERTEX_FLAGS[self.get_mode()]
def get_mode(self) -> str:
"""Returns POLYLINE type as string:
- "AcDb2dPolyline"
- "AcDb3dPolyline"
- "AcDbPolygonMesh"
- "AcDbPolyFaceMesh"
"""
if self.is_3d_polyline:
return "AcDb3dPolyline"
elif self.is_polygon_mesh:
return "AcDbPolygonMesh"
elif self.is_poly_face_mesh:
return "AcDbPolyFaceMesh"
else:
return "AcDb2dPolyline"
@property
def is_2d_polyline(self) -> bool:
"""``True`` if POLYLINE is a 2D polyline."""
return self.dxf.flags & self.ANY3D == 0
@property
def is_3d_polyline(self) -> bool:
"""``True`` if POLYLINE is a 3D polyline."""
return bool(self.dxf.flags & self.POLYLINE_3D)
@property
def is_polygon_mesh(self) -> bool:
"""``True`` if POLYLINE is a polygon mesh, see :class:`Polymesh`"""
return bool(self.dxf.flags & self.POLYMESH)
@property
def is_poly_face_mesh(self) -> bool:
"""``True`` if POLYLINE is a poly face mesh, see :class:`Polyface`"""
return bool(self.dxf.flags & self.POLYFACE)
@property
def is_closed(self) -> bool:
"""``True`` if POLYLINE is closed."""
return bool(self.dxf.flags & self.CLOSED)
@property
def is_m_closed(self) -> bool:
"""``True`` if POLYLINE (as :class:`Polymesh`) is closed in m
direction.
"""
return bool(self.dxf.flags & self.MESH_CLOSED_M_DIRECTION)
@property
def is_n_closed(self) -> bool:
"""``True`` if POLYLINE (as :class:`Polymesh`) is closed in n
direction.
"""
return bool(self.dxf.flags & self.MESH_CLOSED_N_DIRECTION)
@property
def has_arc(self) -> bool:
"""Returns ``True`` if 2D POLYLINE has an arc segment."""
if self.is_2d_polyline:
return any(
v.dxf.hasattr("bulge") and bool(v.dxf.bulge) for v in self.vertices
)
else:
return False
@property
def has_width(self) -> bool:
"""Returns ``True`` if 2D POLYLINE has default width values or any
segment with width attributes.
"""
if self.is_2d_polyline:
if self.dxf.hasattr("default_start_width") and bool(
self.dxf.default_start_width
):
return True
if self.dxf.hasattr("default_end_width") and bool(
self.dxf.default_end_width
):
return True
for v in self.vertices:
if v.dxf.hasattr("start_width") and bool(v.dxf.start_width):
return True
if v.dxf.hasattr("end_width") and bool(v.dxf.end_width):
return True
return False
def m_close(self, status=True) -> None:
"""Close POLYMESH in m direction if `status` is ``True`` (also closes
POLYLINE), clears closed state if `status` is ``False``.
"""
self.set_flag_state(self.MESH_CLOSED_M_DIRECTION, status, name="flags")
def n_close(self, status=True) -> None:
"""Close POLYMESH in n direction if `status` is ``True``, clears closed
state if `status` is ``False``.
"""
self.set_flag_state(self.MESH_CLOSED_N_DIRECTION, status, name="flags")
def close(self, m_close=True, n_close=False) -> None:
"""Set closed state of POLYMESH and POLYLINE in m direction and n
direction. ``True`` set closed flag, ``False`` clears closed flag.
"""
self.m_close(m_close)
self.n_close(n_close)
def __len__(self) -> int:
"""Returns count of :class:`Vertex` entities."""
return len(self.vertices)
def __getitem__(self, pos) -> DXFVertex:
"""Get :class:`Vertex` entity at position `pos`, supports list-like slicing.
"""
return self.vertices[pos]
def points(self) -> Iterator[Vec3]:
"""Returns iterable of all polyline vertices as (x, y, z) tuples,
not as :class:`Vertex` objects.
"""
return (vertex.dxf.location for vertex in self.vertices)
def _append_vertex(self, vertex: DXFVertex) -> None:
self.vertices.append(vertex)
def append_vertices(self, points: Iterable[UVec], dxfattribs=None) -> None:
"""Append multiple :class:`Vertex` entities at location `points`.
Args:
points: iterable of (x, y[, z]) tuples
dxfattribs: dict of DXF attributes for the VERTEX objects
"""
dxfattribs = dict(dxfattribs or {})
for vertex in self._build_dxf_vertices(points, dxfattribs):
self._append_vertex(vertex)
def append_formatted_vertices(
self,
points: Iterable[Sequence],
format: str = "xy",
dxfattribs=None,
) -> None:
"""Append multiple :class:`Vertex` entities at location `points`.
Args:
points: iterable of (x, y, [start_width, [end_width, [bulge]]])
tuple
format: format string, default is "xy", see: :ref:`format codes`
dxfattribs: dict of DXF attributes for the VERTEX objects
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["flags"] = dxfattribs.get("flags", 0) | self.get_vertex_flags()
# same DXF attributes for VERTEX entities as for POLYLINE
dxfattribs["owner"] = self.dxf.owner
dxfattribs["layer"] = self.dxf.layer
if self.dxf.hasattr("linetype"):
dxfattribs["linetype"] = self.dxf.linetype
for point in points:
attribs = vertex_attribs(point, format)
attribs.update(dxfattribs)
vertex = cast(DXFVertex, self._new_compound_entity("VERTEX", attribs))
self._append_vertex(vertex)
def append_vertex(self, point: UVec, dxfattribs=None) -> None:
"""Append a single :class:`Vertex` entity at location `point`.
Args:
point: as (x, y[, z]) tuple
dxfattribs: dict of DXF attributes for :class:`Vertex` class
"""
dxfattribs = dict(dxfattribs or {})
for vertex in self._build_dxf_vertices([point], dxfattribs):
self._append_vertex(vertex)
def insert_vertices(
self, pos: int, points: Iterable[UVec], dxfattribs=None
) -> None:
"""Insert vertices `points` into :attr:`Polyline.vertices` list
at insertion location `pos` .
Args:
pos: insertion position of list :attr:`Polyline.vertices`
points: list of (x, y[, z]) tuples
dxfattribs: dict of DXF attributes for :class:`Vertex` class
"""
dxfattribs = dict(dxfattribs or {})
self.vertices[pos:pos] = list(self._build_dxf_vertices(points, dxfattribs))
def _build_dxf_vertices(
self, points: Iterable[UVec], dxfattribs: dict
) -> Iterator[DXFVertex]:
"""Converts point (x, y, z) tuples into DXFVertex objects.
Args:
points: list of (x, y, z)-tuples
dxfattribs: dict of DXF attributes
"""
dxfattribs["flags"] = dxfattribs.get("flags", 0) | self.get_vertex_flags()
# same DXF attributes for VERTEX entities as for POLYLINE
dxfattribs["owner"] = self.dxf.handle
dxfattribs["layer"] = self.dxf.layer
if self.dxf.hasattr("linetype"):
dxfattribs["linetype"] = self.dxf.linetype
for point in points:
dxfattribs["location"] = Vec3(point)
yield cast(DXFVertex, self._new_compound_entity("VERTEX", dxfattribs))
def cast(self) -> Union[Polyline, Polymesh, Polyface]:
mode = self.get_mode()
if mode == "AcDbPolyFaceMesh":
return Polyface.from_polyline(self)
elif mode == "AcDbPolygonMesh":
return Polymesh.from_polyline(self)
else:
return self
def transform(self, m: Matrix44) -> Polyline:
"""Transform the POLYLINE entity by transformation matrix `m` inplace.
A non-uniform scaling is not supported if a 2D POLYLINE contains
circular arc segments (bulges).
Args:
m: transformation :class:`~ezdxf.math.Matrix44`
Raises:
NonUniformScalingError: for non-uniform scaling of 2D POLYLINE
containing circular arc segments (bulges)
"""
def _ocs_locations(elevation):
for vertex in self.vertices:
location = vertex.dxf.location
if elevation is not None:
# Older DXF versions may not have written the z-axis, so
# replace existing z-axis by the elevation value.
location = location.replace(z=elevation)
yield location
if self.is_2d_polyline:
dxf = self.dxf
ocs = OCSTransform(self.dxf.extrusion, m)
if not ocs.scale_uniform and self.has_arc:
raise NonUniformScalingError(
"2D POLYLINE containing arcs (bulges) does not support non uniform scaling"
)
# The caller function has to catch this exception and explode the
# 2D POLYLINE into LINE and ELLIPSE entities.
if dxf.hasattr("elevation"):
z_axis = dxf.elevation.z
else:
z_axis = None
vertices = [
ocs.transform_vertex(vertex) for vertex in _ocs_locations(z_axis)
]
# All vertices of a 2D polyline must have the same z-axis, which is
# the elevation of the polyline:
if vertices:
dxf.elevation = vertices[0].replace(x=0.0, y=0.0)
for vertex, location in zip(self.vertices, vertices):
vdxf = vertex.dxf
vdxf.location = location
if vdxf.hasattr("start_width"):
vdxf.start_width = ocs.transform_width(vdxf.start_width)
if vdxf.hasattr("end_width"):
vdxf.end_width = ocs.transform_width(vdxf.end_width)
if dxf.hasattr("default_start_width"):
dxf.default_start_width = ocs.transform_width(dxf.default_start_width)
if dxf.hasattr("default_end_width"):
dxf.default_end_width = ocs.transform_width(dxf.default_end_width)
if dxf.hasattr("thickness"):
dxf.thickness = ocs.transform_thickness(dxf.thickness)
dxf.extrusion = ocs.new_extrusion
else:
for vertex in self.vertices:
vertex.transform(m)
self.post_transform(m)
return self
def explode(self, target_layout: Optional[BaseLayout] = None) -> EntityQuery:
"""Explode the POLYLINE entity as DXF primitives (LINE, ARC or 3DFACE)
into the target layout, if the target layout is ``None``, the target
layout is the layout of the POLYLINE entity.
Returns an :class:`~ezdxf.query.EntityQuery` container referencing all
DXF primitives.
Args:
target_layout: target layout for DXF primitives, ``None`` for same
layout as source entity.
"""
return explode_entity(self, target_layout)
def virtual_entities(self) -> Iterator[Union[Line, Arc, Face3d]]:
"""Yields the graphical representation of POLYLINE as virtual DXF
primitives (LINE, ARC or 3DFACE).
These virtual entities are located at the original location, but are not
stored in the entity database, have no handle and are not assigned to
any layout.
"""
for e in virtual_polyline_entities(self):
e.set_source_of_copy(self)
yield e
def audit(self, auditor: Auditor) -> None:
"""Audit and repair the POLYLINE entity."""
def audit_sub_entity(entity):
entity.doc = doc # grant same document
dxf = entity.dxf
if dxf.owner != owner:
dxf.owner = owner
if dxf.layer != layer:
dxf.layer = layer
doc = self.doc
owner = self.dxf.handle
layer = self.dxf.layer
for vertex in self.vertices:
audit_sub_entity(vertex)
seqend = self.seqend
if seqend:
audit_sub_entity(seqend)
elif doc:
self.new_seqend()
auditor.fixed_error(
code=AuditError.MISSING_REQUIRED_SEQEND,
message=f"Created required SEQEND entity for {str(self)}.",
dxf_entity=self,
)
class Polyface(Polyline):
"""
PolyFace structure:
POLYLINE
AcDbEntity
AcDbPolyFaceMesh
VERTEX - Vertex
AcDbEntity
AcDbVertex
AcDbPolyFaceMeshVertex
VERTEX - Face
AcDbEntity
AcDbFaceRecord
SEQEND
Order of mesh_vertices and face_records is important (DXF R2010):
1. mesh_vertices: the polyface mesh vertex locations
2. face_records: indices of the face forming vertices
"""
@classmethod
def from_polyline(cls, polyline: Polyline) -> Polyface:
polyface = cls.shallow_copy(polyline)
polyface._sub_entities = polyline._sub_entities
polyface.seqend = polyline.seqend
# do not destroy polyline - all data would be lost
return polyface
def append_face(self, face: FaceType, dxfattribs=None) -> None:
"""Append a single face. A `face` is a sequence of (x, y, z) tuples.
Args:
face: sequence of (x, y, z) tuples
dxfattribs: dict of DXF attributes for the VERTEX objects
"""
self.append_faces([face], dxfattribs)
def _points_to_dxf_vertices(
self, points: Iterable[UVec], dxfattribs
) -> list[DXFVertex]:
"""Convert (x, y, z) tuples into DXFVertex objects.
Args:
points: sequence of (x, y, z) tuples
dxfattribs: dict of DXF attributes for the VERTEX entity
"""
dxfattribs["flags"] = dxfattribs.get("flags", 0) | self.get_vertex_flags()
# All vertices have to be on the same layer as the POLYLINE entity:
dxfattribs["layer"] = self.get_dxf_attrib("layer", "0")
vertices: list[DXFVertex] = []
for point in points:
dxfattribs["location"] = point
vertices.append(
cast("DXFVertex", self._new_compound_entity("VERTEX", dxfattribs))
)
return vertices
def append_faces(self, faces: Iterable[FaceType], dxfattribs=None) -> None:
"""Append multiple `faces`. `faces` is a list of single faces and a
single face is a sequence of (x, y, z) tuples.
Args:
faces: iterable of sequences of (x, y, z) tuples
dxfattribs: dict of DXF attributes for the VERTEX entity
"""
def new_face_record():
dxfattribs["flags"] = const.VTX_3D_POLYFACE_MESH_VERTEX
# location of face record vertex is always (0, 0, 0)
dxfattribs["location"] = Vec3()
return cast(DXFVertex, self._new_compound_entity("VERTEX", dxfattribs))
dxfattribs = dict(dxfattribs or {})
existing_vertices, existing_faces = self.indexed_faces()
new_faces: list[FaceProxy] = []
for face in faces:
face_mesh_vertices = self._points_to_dxf_vertices(face, {})
# Index of first new vertex
index = len(existing_vertices)
existing_vertices.extend(face_mesh_vertices)
face_record = FaceProxy(new_face_record(), existing_vertices)
# Set VERTEX indices:
face_record.indices = tuple(range(index, index + len(face_mesh_vertices)))
new_faces.append(face_record)
self._rebuild(chain(existing_faces, new_faces))
def _rebuild(self, faces: Iterable[FaceProxy], precision: int = 6) -> None:
"""Build a valid POLYFACE structure from `faces`.
Args:
faces: iterable of FaceProxy objects.
"""
polyface_builder = PolyfaceBuilder(faces, precision=precision)
# Why is list[DXFGraphic] incompatible to list[DXFVertex] when DXFVertex
# is a subclass of DXFGraphic?
self._sub_entities = polyface_builder.get_vertices() # type: ignore
self.update_count(polyface_builder.nvertices, polyface_builder.nfaces)
def update_count(self, nvertices: int, nfaces: int) -> None:
self.dxf.m_count = nvertices
self.dxf.n_count = nfaces
def optimize(self, precision: int = 6) -> None:
"""Rebuilds the :class:`Polyface` by merging vertices with nearly same vertex
locations.
Args:
precision: floating point precision for determining identical
vertex locations
"""
vertices, faces = self.indexed_faces()
self._rebuild(faces, precision)
def faces(self) -> Iterator[list[DXFVertex]]:
"""Iterable of all faces, a face is a tuple of vertices.
Returns:
list of [vertex, vertex, vertex, [vertex,] face_record]
"""
_, faces = self.indexed_faces()
for face in faces:
face_vertices = list(face)
face_vertices.append(face.face_record)
yield face_vertices
def indexed_faces(self) -> tuple[list[DXFVertex], Iterator[FaceProxy]]:
"""Returns a list of all vertices and a generator of FaceProxy()
objects.
(internal API)
"""
vertices: list[DXFVertex] = []
face_records: list[DXFVertex] = []
for vertex in self.vertices:
(vertices if vertex.is_poly_face_mesh_vertex else face_records).append(
vertex
)
faces = (FaceProxy(face_record, vertices) for face_record in face_records)
return vertices, faces
class FaceProxy:
"""Represents a single face of a polyface structure. (internal class)
vertices:
List of all polyface vertices.
face_record:
The face forming vertex of type ``AcDbFaceRecord``, contains the indices
to the face building vertices. Indices of the DXF structure are 1-based
and a negative index indicates the beginning of an invisible edge.
Face.face_record.dxf.color determines the color of the face.
indices:
Indices to the face building vertices as tuple. This indices are 0-base
and are used to get vertices from the list `Face.vertices`.
"""
__slots__ = ("vertices", "face_record", "indices")
def __init__(self, face_record: DXFVertex, vertices: Sequence[DXFVertex]):
"""Returns iterable of all face vertices as :class:`Vertex` entities."""
self.vertices: Sequence[DXFVertex] = vertices
self.face_record: DXFVertex = face_record
self.indices: Sequence[int] = self._indices()
def __len__(self) -> int:
"""Returns count of face vertices (without face_record)."""
return len(self.indices)
def __getitem__(self, pos: int) -> DXFVertex:
"""Returns :class:`Vertex` at position `pos`.
Args:
pos: vertex position 0-based
"""
return self.vertices[self.indices[pos]]
def __iter__(self) -> Iterator["DXFVertex"]:
return (self.vertices[index] for index in self.indices)
def points(self) -> Iterator[UVec]:
"""Returns iterable of all face vertex locations as (x, y, z)-tuples."""
return (vertex.dxf.location for vertex in self)
def _raw_indices(self) -> Iterable[int]:
return (self.face_record.get_dxf_attrib(name, 0) for name in const.VERTEXNAMES)
def _indices(self) -> Sequence[int]:
return tuple(abs(index) - 1 for index in self._raw_indices() if index != 0)
def is_edge_visible(self, pos: int) -> bool:
"""Returns ``True`` if edge starting at vertex `pos` is visible.
Args:
pos: vertex position 0-based
"""
name = const.VERTEXNAMES[pos]
return self.face_record.get_dxf_attrib(name) > 0
class PolyfaceBuilder:
"""Optimized POLYFACE builder. (internal class)"""
def __init__(self, faces: Iterable[FaceProxy], precision: int = 6):
self.precision: int = precision
self.faces: list[DXFVertex] = []
self.vertices: list[DXFVertex] = []
self.index_mapping: dict[tuple[float, ...], int] = {}
self.build(faces)
@property
def nvertices(self) -> int:
return len(self.vertices)
@property
def nfaces(self) -> int:
return len(self.faces)
def get_vertices(self) -> list[DXFVertex]:
vertices = self.vertices[:]
vertices.extend(self.faces)
return vertices
def build(self, faces: Iterable[FaceProxy]) -> None:
for face in faces:
face_record = face.face_record
for vertex, name in zip(face, VERTEXNAMES):
index = self.add(vertex)
# preserve sign of old index value
sign = -1 if face_record.dxf.get(name, 0) < 0 else +1
face_record.dxf.set(name, (index + 1) * sign)
self.faces.append(face_record)
def add(self, vertex: DXFVertex) -> int:
def key(point):
return tuple((round(coord, self.precision) for coord in point))
location = key(vertex.dxf.location)
try:
return self.index_mapping[location]
except KeyError:
index = len(self.vertices)
self.index_mapping[location] = index
self.vertices.append(vertex)
return index
class Polymesh(Polyline):
"""
PolyMesh structure:
POLYLINE
AcDbEntity
AcDbPolygonMesh
VERTEX
AcDbEntity
AcDbVertex
AcDbPolygonMeshVertex
"""
@classmethod
def from_polyline(cls, polyline: Polyline) -> Polymesh:
polymesh = cls.shallow_copy(polyline)
polymesh._sub_entities = polyline._sub_entities
polymesh.seqend = polyline.seqend
return polymesh
def set_mesh_vertex(self, pos: tuple[int, int], point: UVec, dxfattribs=None):
"""Set location and DXF attributes of a single mesh vertex.
Args:
pos: 0-based (row, col) tuple, position of mesh vertex
point: (x, y, z) tuple, new 3D coordinates of the mesh vertex
dxfattribs: dict of DXF attributes
"""
dxfattribs = dict(dxfattribs or {})
dxfattribs["location"] = point
vertex = self.get_mesh_vertex(pos)
vertex.update_dxf_attribs(dxfattribs)
def get_mesh_vertex(self, pos: tuple[int, int]) -> DXFVertex:
"""Get location of a single mesh vertex.
Args:
pos: 0-based (row, col) tuple, position of mesh vertex
"""
m_count = self.dxf.m_count
n_count = self.dxf.n_count
m, n = pos
if 0 <= m < m_count and 0 <= n < n_count:
return self.vertices[m * n_count + n]
else:
raise const.DXFIndexError(repr(pos))
def get_mesh_vertex_cache(self) -> MeshVertexCache:
"""Get a :class:`MeshVertexCache` object for this POLYMESH.
The caching object provides fast access to the :attr:`location`
attribute of mesh vertices.
"""
return MeshVertexCache(self)
class MeshVertexCache:
"""Cache mesh vertices in a dict, keys are 0-based (row, col)-tuples.
vertices:
Dict of mesh vertices, keys are 0-based (row, col)-tuples. Writing to
this dict doesn't change the DXF entity.
"""
__slots__ = ("vertices",)
def __init__(self, mesh: Polyline):
self.vertices: dict[tuple[int, int], DXFVertex] = self._setup(
mesh, mesh.dxf.m_count, mesh.dxf.n_count
)
def _setup(self, mesh: Polyline, m_count: int, n_count: int) -> dict:
cache: dict[tuple[int, int], DXFVertex] = {}
vertices = iter(mesh.vertices)
for m in range(m_count):
for n in range(n_count):
cache[(m, n)] = next(vertices)
return cache
def __getitem__(self, pos: tuple[int, int]) -> UVec:
"""Get mesh vertex location as (x, y, z)-tuple.
Args:
pos: 0-based (row, col)-tuple.
"""
try:
return self.vertices[pos].dxf.location
except KeyError:
raise const.DXFIndexError(repr(pos))
def __setitem__(self, pos: tuple[int, int], location: UVec) -> None:
"""Get mesh vertex location as (x, y, z)-tuple.
Args:
pos: 0-based (row, col)-tuple.
location: (x, y, z)-tuple
"""
try:
self.vertices[pos].dxf.location = location
except KeyError:
raise const.DXFIndexError(repr(pos))
acdb_vertex = DefSubclass(
"AcDbVertex",
{ # last subclass index -1
# Location point in OCS if 2D, and WCS if 3D
"location": DXFAttr(10, xtype=XType.point3d),
"start_width": DXFAttr(40, default=0, optional=True),
"end_width": DXFAttr(41, default=0, optional=True),
# Bulge (optional; default is 0). The bulge is the tangent of one fourth
# the included angle for an arc segment, made negative if the arc goes
# clockwise from the start point to the endpoint. A bulge of 0 indicates
# a straight segment, and a bulge of 1 is a semicircle.
"bulge": DXFAttr(42, default=0, optional=True),
"flags": DXFAttr(70, default=0),
# Curve fit tangent direction (in degrees)
"tangent": DXFAttr(50, optional=True),
"vtx0": DXFAttr(71, optional=True),
"vtx1": DXFAttr(72, optional=True),
"vtx2": DXFAttr(73, optional=True),
"vtx3": DXFAttr(74, optional=True),
"vertex_identifier": DXFAttr(91, optional=True),
},
)
acdb_vertex_group_codes = group_code_mapping(acdb_vertex)
merged_vertex_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_vertex_group_codes # type: ignore
)
@factory.register_entity
class DXFVertex(DXFGraphic):
"""DXF VERTEX entity"""
DXFTYPE = "VERTEX"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_vertex)
# Extra vertex created by curve-fitting:
EXTRA_VERTEX_CREATED = 1
# Curve-fit tangent defined for this vertex. A curve-fit tangent direction
# of 0 may be omitted from the DXF output, but is significant if this bit
# is set:
CURVE_FIT_TANGENT = 2
# 4 = unused, never set in dxf files
# Spline vertex created by spline-fitting
SPLINE_VERTEX_CREATED = 8
SPLINE_FRAME_CONTROL_POINT = 16
POLYLINE_3D_VERTEX = 32
POLYGON_MESH_VERTEX = 64
POLYFACE_MESH_VERTEX = 128
FACE_FLAGS = POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX
VTX3D = POLYLINE_3D_VERTEX + POLYGON_MESH_VERTEX + POLYFACE_MESH_VERTEX
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
"""Loading interface. (internal API)"""
# bypass DXFGraphic, loading proxy graphic is skipped!
dxf = super(DXFGraphic, self).load_dxf_attribs(processor)
if processor:
processor.simple_dxfattribs_loader(dxf, merged_vertex_group_codes)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
# VERTEX can have 3 subclasses if representing a `face record` or
# 4 subclasses if representing a vertex location, just the last
# subclass contains data
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
if self.is_face_record:
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbFaceRecord")
else:
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbVertex")
if self.is_3d_polyline_vertex:
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDb3dPolylineVertex")
elif self.is_poly_face_mesh_vertex:
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbPolyFaceMeshVertex")
elif self.is_polygon_mesh_vertex:
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDbPolygonMeshVertex")
else:
tagwriter.write_tag2(SUBCLASS_MARKER, "AcDb2dVertex")
self.dxf.export_dxf_attribs(
tagwriter,
[
"location",
"start_width",
"end_width",
"bulge",
"flags",
"tangent",
"vtx0",
"vtx1",
"vtx2",
"vtx3",
"vertex_identifier",
],
)
@property
def is_2d_polyline_vertex(self) -> bool:
return self.dxf.flags & self.VTX3D == 0
@property
def is_3d_polyline_vertex(self) -> bool:
return self.dxf.flags & self.POLYLINE_3D_VERTEX
@property
def is_polygon_mesh_vertex(self) -> bool:
return self.dxf.flags & self.POLYGON_MESH_VERTEX
@property
def is_poly_face_mesh_vertex(self) -> bool:
return self.dxf.flags & self.FACE_FLAGS == self.FACE_FLAGS
@property
def is_face_record(self) -> bool:
return (self.dxf.flags & self.FACE_FLAGS) == self.POLYFACE_MESH_VERTEX
def transform(self, m: Matrix44) -> DXFVertex:
"""Transform the VERTEX entity by transformation matrix `m` inplace."""
if self.is_face_record:
return self
self.dxf.location = m.transform(self.dxf.location)
return self
def format(self, format="xyz") -> Sequence:
"""Return formatted vertex components as tuple.
Format codes:
- ``x`` = x-coordinate
- ``y`` = y-coordinate
- ``z`` = z-coordinate
- ``s`` = start width
- ``e`` = end width
- ``b`` = bulge value
- ``v`` = (x, y, z) as tuple
Args:
format: format string, default is "xyz"
"""
dxf = self.dxf
v = Vec3(dxf.location)
x, y, z = v.xyz
b = dxf.bulge
s = dxf.start_width
e = dxf.end_width
vars = locals()
return tuple(vars[code] for code in format.lower())
def vertex_attribs(data: Sequence, format="xyseb") -> dict:
"""Create VERTEX attributes from input data.
Format codes:
- ``x`` = x-coordinate
- ``y`` = y-coordinate
- ``s`` = start width
- ``e`` = end width
- ``b`` = bulge value
- ``v`` = (x, y [,z]) tuple (z-axis is ignored)
Args:
data: list or tuple of point components
format: format string, default is 'xyseb'
Returns:
dict with keys: 'location', 'bulge', 'start_width', 'end_width'
"""
attribs = dict()
format = [code for code in format.lower() if code in FORMAT_CODES]
location = Vec3()
for code, value in zip(format, data):
if code not in FORMAT_CODES:
continue
if code == "v":
location = Vec3(value)
elif code == "b":
attribs["bulge"] = float(value)
elif code == "s":
attribs["start_width"] = float(value)
elif code == "e":
attribs["end_width"] = float(value)
elif code == "x":
location = location.replace(x=float(value))
elif code == "y":
location = location.replace(y=float(value))
attribs["location"] = location
return attribs