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

488 lines
17 KiB
Python

# Copyright (c) 2019-2024 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import (
TYPE_CHECKING,
Iterable,
Sequence,
Union,
Iterator,
Optional,
)
from typing_extensions import Self
import array
import copy
from itertools import chain
from contextlib import contextmanager
from ezdxf.audit import AuditError
from ezdxf.lldxf import validator
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
RETURN_DEFAULT,
group_code_mapping,
)
from ezdxf.lldxf.const import (
SUBCLASS_MARKER,
DXF2000,
DXFValueError,
DXFStructureError,
DXFIndexError,
)
from ezdxf.lldxf.packedtags import VertexArray, TagArray, TagList
from ezdxf.math import Matrix44, UVec, Vec3
from ezdxf.tools import take2
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import DXFGraphic, acdb_entity
from .factory import register_entity
from .copy import default_copy
if TYPE_CHECKING:
from ezdxf.entities import DXFNamespace, DXFEntity
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.lldxf.tags import Tags
from ezdxf.audit import Auditor
__all__ = ["Mesh", "MeshData"]
acdb_mesh = DefSubclass(
"AcDbSubDMesh",
{
"version": DXFAttr(71, default=2),
"blend_crease": DXFAttr(
72,
default=0,
validator=validator.is_integer_bool,
fixer=RETURN_DEFAULT,
),
# 0 is no smoothing
"subdivision_levels": DXFAttr(
91,
default=0,
validator=validator.is_greater_or_equal_zero,
fixer=RETURN_DEFAULT,
),
# 92: Vertex count of level 0
# 10: Vertex position, multiple entries
# 93: Size of face list of level 0
# 90: Face list item, >=3 possible
# 90: length of face list
# 90: 1st vertex index
# 90: 2nd vertex index ...
# 94: Edge count of level 0
# 90: Vertex index of 1st edge
# 90: Vertex index of 2nd edge
# 95: Edge crease count of level 0
# 95 same as 94, or how is the 'edge create value' associated to edge index
# 140: Edge crease value
#
# Overriding properties: how does this work?
# 90: Count of sub-entity which property has been overridden
# 91: Sub-entity marker
# 92: Count of property was overridden
# 90: Property type
# 0 = Color
# 1 = Material
# 2 = Transparency
# 3 = Material mapper
},
)
acdb_mesh_group_codes = group_code_mapping(acdb_mesh)
class EdgeArray(TagArray):
DTYPE = "L"
def __len__(self) -> int:
return len(self.values) // 2
def __iter__(self) -> Iterator[tuple[int, int]]:
for edge in take2(self.values):
yield edge
def set_data(self, edges: Iterable[tuple[int, int]]) -> None:
self.values = array.array(self.DTYPE, chain.from_iterable(edges))
def export_dxf(self, tagwriter: AbstractTagWriter):
# count = count of edges not tags!
tagwriter.write_tag2(94, len(self.values) // 2)
for index in self.values:
tagwriter.write_tag2(90, index)
class FaceList(TagList):
def __len__(self) -> int:
return len(self.values)
def __iter__(self) -> Iterable[array.array]:
return iter(self.values)
def export_dxf(self, tagwriter: AbstractTagWriter):
# count = count of tags not faces!
tagwriter.write_tag2(93, self.tag_count())
for face in self.values:
tagwriter.write_tag2(90, len(face))
for index in face:
tagwriter.write_tag2(90, index)
def tag_count(self) -> int:
return len(self.values) + sum(len(f) for f in self.values)
def set_data(self, faces: Iterable[Sequence[int]]) -> None:
_faces = []
for face in faces:
_faces.append(face_to_array(face))
self.values = _faces
def face_to_array(face: Sequence[int]) -> array.array:
max_index = max(face)
if max_index < 256:
dtype = "B"
elif max_index < 65536:
dtype = "I"
else:
dtype = "L"
return array.array(dtype, face)
def create_vertex_array(tags: Tags, start_index: int) -> VertexArray:
vertex_tags = tags.collect_consecutive_tags(codes=(10,), start=start_index)
return VertexArray(data=[t.value for t in vertex_tags])
def create_face_list(tags: Tags, start_index: int) -> FaceList:
faces = FaceList()
faces_list = faces.values
face: list[int] = []
counter = 0
for tag in tags.collect_consecutive_tags(codes=(90,), start=start_index):
if not counter:
# leading counter tag
counter = tag.value
if face:
# group code 90 = 32 bit integer
faces_list.append(face_to_array(face))
face = []
else:
# followed by count face tags
counter -= 1
face.append(tag.value)
# add last face
if face:
# group code 90 = 32 bit integer
faces_list.append(face_to_array(face))
return faces
def create_edge_array(tags: Tags, start_index: int) -> EdgeArray:
return EdgeArray(data=collect_values(tags, start_index, code=90)) # int values
def collect_values(
tags: Tags, start_index: int, code: int
) -> Iterable[Union[float, int]]:
values = tags.collect_consecutive_tags(codes=(code,), start=start_index)
return (t.value for t in values)
def create_crease_array(tags: Tags, start_index: int) -> array.array:
return array.array("f", collect_values(tags, start_index, code=140)) # float values
COUNT_ERROR_MSG = "'MESH (#{}) without {} count.'"
@register_entity
class Mesh(DXFGraphic):
"""DXF MESH entity"""
DXFTYPE = "MESH"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_mesh)
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
def __init__(self):
super().__init__()
self._vertices = VertexArray() # vertices stored as array.array('d')
self._faces = FaceList() # face lists data
self._edges = EdgeArray() # edge indices stored as array.array('L')
self._creases = array.array("f") # creases stored as array.array('f')
def copy_data(self, entity: Self, copy_strategy=default_copy) -> None:
"""Copy data: vertices, faces, edges, creases."""
assert isinstance(entity, Mesh)
entity._vertices = copy.deepcopy(self._vertices)
entity._faces = copy.deepcopy(self._faces)
entity._edges = copy.deepcopy(self._edges)
entity._creases = copy.deepcopy(self._creases)
def load_dxf_attribs(
self, processor: Optional[SubclassProcessor] = None
) -> DXFNamespace:
dxf = super().load_dxf_attribs(processor)
if processor:
tags = processor.subclass_by_index(2)
if tags:
# Load mesh data and remove their tags from subclass
self.load_mesh_data(tags, dxf.handle)
# Load remaining data into name space
processor.fast_load_dxfattribs(
dxf, acdb_mesh_group_codes, 2, recover=True
)
else:
raise DXFStructureError(
f"missing 'AcDbSubMesh' subclass in MESH(#{dxf.handle})"
)
return dxf
def load_mesh_data(self, mesh_tags: Tags, handle: str) -> None:
def process_vertices():
try:
vertex_count_index = mesh_tags.tag_index(92)
except DXFValueError:
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "vertex"))
vertices = create_vertex_array(mesh_tags, vertex_count_index + 1)
# Remove vertex count tag and all vertex tags
end_index = vertex_count_index + 1 + len(vertices)
del mesh_tags[vertex_count_index:end_index]
return vertices
def process_faces():
try:
face_count_index = mesh_tags.tag_index(93)
except DXFValueError:
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "face"))
else:
# Remove face count tag and all face tags
faces = create_face_list(mesh_tags, face_count_index + 1)
end_index = face_count_index + 1 + faces.tag_count()
del mesh_tags[face_count_index:end_index]
return faces
def process_edges():
try:
edge_count_index = mesh_tags.tag_index(94)
except DXFValueError:
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "edge"))
else:
edges = create_edge_array(mesh_tags, edge_count_index + 1)
# Remove edge count tag and all edge tags
end_index = edge_count_index + 1 + len(edges.values)
del mesh_tags[edge_count_index:end_index]
return edges
def process_creases():
try:
crease_count_index = mesh_tags.tag_index(95)
except DXFValueError:
raise DXFStructureError(COUNT_ERROR_MSG.format(handle, "crease"))
else:
creases = create_crease_array(mesh_tags, crease_count_index + 1)
# Remove crease count tag and all crease tags
end_index = crease_count_index + 1 + len(creases)
del mesh_tags[crease_count_index:end_index]
return creases
self._vertices = process_vertices()
self._faces = process_faces()
self._edges = process_edges()
self._creases = process_creases()
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_mesh.name)
self.dxf.export_dxf_attribs(
tagwriter, ["version", "blend_crease", "subdivision_levels"]
)
self.export_mesh_data(tagwriter)
self.export_override_data(tagwriter)
def export_mesh_data(self, tagwriter: AbstractTagWriter):
tagwriter.write_tag2(92, len(self.vertices))
self._vertices.export_dxf(tagwriter, code=10)
self._faces.export_dxf(tagwriter)
self._edges.export_dxf(tagwriter)
creases = self._fixed_crease_values()
tagwriter.write_tag2(95, len(self.creases))
for crease_value in creases:
tagwriter.write_tag2(140, crease_value)
def _fixed_crease_values(self) -> list[float]:
# The edge count has to match the crease count, otherwise its an invalid
# DXF file to AutoCAD!
edge_count = len(self._edges)
creases = list(self.creases)
crease_count = len(creases)
if edge_count < crease_count:
creases = creases[:edge_count]
while edge_count > len(creases):
creases.append(0.0)
return creases
def export_override_data(self, tagwriter: AbstractTagWriter):
tagwriter.write_tag2(90, 0)
@property
def creases(self) -> array.array:
"""Creases as :class:`array.array`. (read/write)"""
return self._creases
@creases.setter
def creases(self, values: Iterable[float]) -> None:
self._creases = array.array("f", values)
@property
def vertices(self):
"""Vertices as list like :class:`~ezdxf.lldxf.packedtags.VertexArray`.
(read/write)
"""
return self._vertices
@vertices.setter
def vertices(self, points: Iterable[UVec]) -> None:
self._vertices = VertexArray(points)
@property
def edges(self):
"""Edges as list like :class:`~ezdxf.lldxf.packedtags.TagArray`.
(read/write)
"""
return self._edges
@edges.setter
def edges(self, edges: Iterable[tuple[int, int]]) -> None:
self._edges.set_data(edges)
@property
def faces(self):
"""Faces as list like :class:`~ezdxf.lldxf.packedtags.TagList`.
(read/write)
"""
return self._faces
@faces.setter
def faces(self, faces: Iterable[Sequence[int]]) -> None:
self._faces.set_data(faces)
def get_data(self) -> MeshData:
return MeshData(self)
def set_data(self, data: MeshData) -> None:
self.vertices = data.vertices
self._faces.set_data(data.faces)
self._edges.set_data(data.edges)
self.creases = array.array("f", data.edge_crease_values)
if len(self.edges) != len(self.creases):
raise DXFValueError("count of edges must match count of creases")
@contextmanager
def edit_data(self) -> Iterator[MeshData]:
"""Context manager for various mesh data, returns a :class:`MeshData` instance.
Despite that vertices, edge and faces are accessible as packed data types, the
usage of :class:`MeshData` by context manager :meth:`edit_data` is still
recommended.
"""
data = self.get_data()
yield data
self.set_data(data)
def transform(self, m: Matrix44) -> Mesh:
"""Transform the MESH entity by transformation matrix `m` inplace."""
self._vertices.transform(m)
self.post_transform(m)
return self
def audit(self, auditor: Auditor) -> None:
if not self.is_alive:
return
super().audit(auditor)
if len(self.edges) != len(self.creases):
self.creases = self._fixed_crease_values() # type: ignore
auditor.fixed_error(
code=AuditError.INVALID_CREASE_VALUE_COUNT,
message=f"fixed invalid count of crease values in {str(self)}",
dxf_entity=self,
)
class MeshData:
def __init__(self, mesh: Mesh) -> None:
self.vertices: list[Vec3] = Vec3.list(mesh.vertices)
self.faces: list[Sequence[int]] = list(mesh.faces)
self.edges: list[tuple[int, int]] = list(mesh.edges)
self.edge_crease_values: list[float] = list(mesh.creases)
def add_face(self, vertices: Iterable[UVec]) -> Sequence[int]:
"""Add a face by a list of vertices."""
indices = tuple(self.add_vertex(vertex) for vertex in vertices)
self.faces.append(indices)
return indices
def add_edge_crease(self, v1: int, v2: int, crease: float):
"""Add an edge crease value, the edge is defined by the vertex indices
`v1` and `v2`.
The crease value defines the amount of subdivision that will be applied
to this edge. A crease value of the subdivision level prevents the edge from
deformation and a value of 0.0 means no protection from subdividing.
"""
if v1 < 0 or v1 > len(self.vertices):
raise DXFIndexError("vertex index `v1` out of range")
if v2 < 0 or v2 > len(self.vertices):
raise DXFIndexError("vertex index `v2` out of range")
self.edges.append((v1, v2))
self.edge_crease_values.append(crease)
def add_vertex(self, vertex: UVec) -> int:
if len(vertex) != 3:
raise DXFValueError("Parameter vertex has to be a 3-tuple (x, y, z).")
index = len(self.vertices)
self.vertices.append(Vec3(vertex))
return index
def optimize(self):
"""Reduce vertex count by merging coincident vertices."""
def merge_coincident_vertices() -> dict[int, int]:
original_vertices = [
(v.xyz, index, v) for index, v in enumerate(self.vertices)
]
original_vertices.sort()
self.vertices = []
index_map: dict[int, int] = {}
prev_vertex = sentinel = Vec3()
index = 0
for _, original_index, vertex in original_vertices:
if prev_vertex is sentinel or not vertex.isclose(prev_vertex):
index = len(self.vertices)
self.vertices.append(vertex)
index_map[original_index] = index
prev_vertex = vertex
else: # this is a coincident vertex
index_map[original_index] = index
return index_map
def remap_faces() -> None:
self.faces = remap_indices(self.faces)
def remap_edges() -> None:
self.edges = remap_indices(self.edges) # type: ignore
def remap_indices(indices: Sequence[Sequence[int]]) -> list[Sequence[int]]:
mapped_indices: list[Sequence[int]] = []
for entry in indices:
mapped_indices.append(tuple(index_map[index] for index in entry))
return mapped_indices
index_map = merge_coincident_vertices()
remap_faces()
remap_edges()