488 lines
17 KiB
Python
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()
|