1140 lines
38 KiB
Python
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
|