416 lines
15 KiB
Python
416 lines
15 KiB
Python
# Copyright (c) 2022-2024, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import Iterator, Sequence, Optional, Iterable
|
|
from ezdxf.render import MeshVertexMerger, MeshTransformer, MeshBuilder
|
|
from ezdxf.math import Matrix44, Vec3, NULLVEC, BoundingBox
|
|
from . import entities
|
|
from .entities import Body, Lump, NONE_REF, Face, Shell
|
|
|
|
|
|
def mesh_from_body(body: Body, merge_lumps=True) -> list[MeshTransformer]:
|
|
"""Returns a list of :class:`~ezdxf.render.MeshTransformer` instances from
|
|
the given ACIS :class:`Body` entity.
|
|
The list contains multiple meshes if `merge_lumps` is ``False`` or just a
|
|
single mesh if `merge_lumps` is ``True``.
|
|
|
|
The ACIS format stores the faces in counter-clockwise orientation where the
|
|
face-normal points outwards (away) from the solid body (material).
|
|
|
|
.. note::
|
|
|
|
This function returns meshes build up only from flat polygonal
|
|
:class:`Face` entities, for a tessellation of more complex ACIS
|
|
entities (spline surfaces, tori, cones, ...) is an ACIS kernel
|
|
required which `ezdxf` does not provide.
|
|
|
|
Args:
|
|
body: ACIS entity of type :class:`Body`
|
|
merge_lumps: returns all :class:`Lump` entities
|
|
from a body as a single mesh if ``True`` otherwise each :class:`Lump`
|
|
entity is a separated mesh
|
|
|
|
Raises:
|
|
TypeError: given `body` entity has invalid type
|
|
|
|
"""
|
|
if not isinstance(body, Body):
|
|
raise TypeError(f"expected a body entity, got: {type(body)}")
|
|
|
|
meshes: list[MeshTransformer] = []
|
|
builder = MeshVertexMerger()
|
|
for faces in flat_polygon_faces_from_body(body):
|
|
for face in faces:
|
|
builder.add_face(face)
|
|
if not merge_lumps:
|
|
meshes.append(MeshTransformer.from_builder(builder))
|
|
builder = MeshVertexMerger()
|
|
if merge_lumps:
|
|
meshes.append(MeshTransformer.from_builder(builder))
|
|
return meshes
|
|
|
|
|
|
def flat_polygon_faces_from_body(
|
|
body: Body,
|
|
) -> Iterator[list[Sequence[Vec3]]]:
|
|
"""Yields all flat polygon faces from all lumps in the given
|
|
:class:`Body` entity.
|
|
Yields a separated list of faces for each linked :class:`Lump` entity.
|
|
|
|
Args:
|
|
body: ACIS entity of type :class:`Body`
|
|
|
|
Raises:
|
|
TypeError: given `body` entity has invalid type
|
|
|
|
"""
|
|
|
|
if not isinstance(body, Body):
|
|
raise TypeError(f"expected a body entity, got: {type(body)}")
|
|
lump = body.lump
|
|
transform = body.transform
|
|
|
|
m: Optional[Matrix44] = None
|
|
if not transform.is_none:
|
|
m = transform.matrix
|
|
while not lump.is_none:
|
|
yield list(flat_polygon_faces_from_lump(lump, m))
|
|
lump = lump.next_lump
|
|
|
|
|
|
def flat_polygon_faces_from_lump(
|
|
lump: Lump, m: Matrix44 | None = None
|
|
) -> Iterator[Sequence[Vec3]]:
|
|
"""Yields all flat polygon faces from the given :class:`Lump` entity as
|
|
sequence of :class:`~ezdxf.math.Vec3` instances. Applies the transformation
|
|
:class:`~ezdxf.math.Matrix44` `m` to all vertices if not ``None``.
|
|
|
|
Args:
|
|
lump: :class:`Lump` entity
|
|
m: optional transformation matrix
|
|
|
|
Raises:
|
|
TypeError: `lump` has invalid ACIS type
|
|
|
|
"""
|
|
if not isinstance(lump, Lump):
|
|
raise TypeError(f"expected a lump entity, got: {type(lump)}")
|
|
|
|
shell = lump.shell
|
|
if shell.is_none:
|
|
return # not a shell
|
|
vertices: list[Vec3] = []
|
|
face = shell.face
|
|
while not face.is_none:
|
|
first_coedge = NONE_REF
|
|
vertices.clear()
|
|
if face.surface.type == "plane-surface":
|
|
try:
|
|
first_coedge = face.loop.coedge
|
|
except AttributeError: # loop is a none-entity
|
|
pass
|
|
coedge = first_coedge
|
|
while not coedge.is_none: # invalid coedge or face is not closed
|
|
# the edge entity contains the vertices and the curve type
|
|
edge = coedge.edge
|
|
try:
|
|
# only straight lines as face edges supported:
|
|
if edge.curve.type != "straight-curve":
|
|
break
|
|
# add the first edge vertex to the face vertices
|
|
if coedge.sense: # reversed sense of the underlying edge
|
|
vertices.append(edge.end_vertex.point.location)
|
|
else: # same sense as the underlying edge
|
|
vertices.append(edge.start_vertex.point.location)
|
|
except AttributeError:
|
|
# one of the involved entities is a none-entity or an
|
|
# incompatible entity type -> ignore this face!
|
|
break
|
|
coedge = coedge.next_coedge
|
|
if coedge is first_coedge: # a valid closed face
|
|
if m is not None:
|
|
yield tuple(m.transform_vertices(vertices))
|
|
else:
|
|
yield tuple(vertices)
|
|
break
|
|
face = face.next_face
|
|
|
|
|
|
def body_from_mesh(mesh: MeshBuilder, precision: int = 6) -> Body:
|
|
"""Returns a :term:`ACIS` :class:`~ezdxf.acis.entities.Body` entity from a
|
|
:class:`~ezdxf.render.MeshBuilder` instance.
|
|
|
|
This entity can be assigned to a :class:`~ezdxf.entities.Solid3d` DXF entity
|
|
as :term:`SAT` or :term:`SAB` data according to the version your DXF
|
|
document uses (SAT for DXF R2000 to R2010 and SAB for DXF R2013 and later).
|
|
|
|
If the `mesh` contains multiple separated meshes, each mesh will be a
|
|
separated :class:`~ezdxf.acis.entities.Lump` node.
|
|
If each mesh should get its own :class:`~ezdxf.acis.entities.Body` entity,
|
|
separate the meshes beforehand by the method
|
|
:meth:`~ezdxf.render.MeshBuilder.separate_meshes`.
|
|
|
|
A closed mesh creates a solid body and an open mesh creates an open (hollow)
|
|
shell. The detection if the mesh is open or closed is based on the edges
|
|
of the mesh: if **all** edges of mesh have two adjacent faces the mesh is
|
|
closed.
|
|
|
|
The current implementation applies automatically a vertex optimization,
|
|
which merges coincident vertices into a single vertex.
|
|
|
|
"""
|
|
mesh = mesh.optimize_vertices(precision)
|
|
body = Body()
|
|
bbox = BoundingBox(mesh.vertices)
|
|
if not bbox.center.is_null:
|
|
mesh.translate(-bbox.center)
|
|
transform = entities.Transform()
|
|
transform.matrix = Matrix44.translate(*bbox.center)
|
|
body.transform = transform
|
|
|
|
for mesh in mesh.separate_meshes():
|
|
lump = lump_from_mesh(mesh)
|
|
body.append_lump(lump)
|
|
return body
|
|
|
|
|
|
def lump_from_mesh(mesh: MeshBuilder) -> Lump:
|
|
"""Returns a :class:`~ezdxf.acis.entities.Lump` entity from a
|
|
:class:`~ezdxf.render.MeshBuilder` instance. The `mesh` has to be a single
|
|
body or shell!
|
|
"""
|
|
lump = Lump()
|
|
shell = Shell()
|
|
lump.append_shell(shell)
|
|
face_builder = PolyhedronFaceBuilder(mesh)
|
|
for face in face_builder.acis_faces():
|
|
shell.append_face(face)
|
|
return lump
|
|
|
|
|
|
class PolyhedronFaceBuilder:
|
|
def __init__(self, mesh: MeshBuilder):
|
|
mesh_copy = mesh.copy()
|
|
mesh_copy.normalize_faces() # open faces without duplicates!
|
|
self.vertices: list[Vec3] = mesh_copy.vertices
|
|
self.faces: list[Sequence[int]] = mesh_copy.faces
|
|
self.normals = list(mesh_copy.face_normals())
|
|
self.acis_vertices: list[entities.Vertex] = []
|
|
|
|
# double_sided:
|
|
# If every edge belongs to two faces the body is for sure a closed
|
|
# surface. But the "is_edge_balance_broken" property can not detect
|
|
# non-manifold meshes!
|
|
# - True: the body is an open shell, each side of the face is outside
|
|
# (environment side)
|
|
# - False: the body is a closed solid body, one side points outwards of
|
|
# the body (environment side) and one side points inwards (material
|
|
# side)
|
|
self.double_sided = mesh_copy.diagnose().is_edge_balance_broken
|
|
|
|
# coedges and edges ledger, where index1 <= index2
|
|
self.partner_coedges: dict[tuple[int, int], entities.Coedge] = dict()
|
|
self.edges: dict[tuple[int, int], entities.Edge] = dict()
|
|
|
|
def reset(self):
|
|
self.acis_vertices = list(make_vertices(self.vertices))
|
|
self.partner_coedges.clear()
|
|
self.edges.clear()
|
|
|
|
def acis_faces(self) -> list[Face]:
|
|
self.reset()
|
|
faces: list[Face] = []
|
|
for face, face_normal in zip(self.faces, self.normals):
|
|
if face_normal.is_null:
|
|
continue
|
|
acis_face = Face()
|
|
plane = self.make_plane(face)
|
|
if plane is None:
|
|
continue
|
|
plane.normal = face_normal
|
|
loop = self.make_loop(face)
|
|
if loop is None:
|
|
continue
|
|
acis_face.append_loop(loop)
|
|
acis_face.surface = plane
|
|
acis_face.sense = False # face normal is plane normal
|
|
acis_face.double_sided = self.double_sided
|
|
faces.append(acis_face)
|
|
# The link structure of all entities is only completed at the end of
|
|
# the building process. Do not yield faces from the body of the loop!
|
|
return faces
|
|
|
|
def make_plane(self, face: Sequence[int]) -> Optional[entities.Plane]:
|
|
assert len(face) > 1, "face requires least 2 vertices"
|
|
plane = entities.Plane()
|
|
# normal is always calculated by the right-hand rule:
|
|
plane.reverse_v = False
|
|
plane.origin = self.vertices[face[0]]
|
|
try:
|
|
plane.u_dir = (self.vertices[face[1]] - plane.origin).normalize()
|
|
except ZeroDivisionError:
|
|
return None # vertices are too close together
|
|
return plane
|
|
|
|
def make_loop(self, face: Sequence[int]) -> Optional[entities.Loop]:
|
|
coedges: list[entities.Coedge] = []
|
|
face2 = list(face[1:])
|
|
if face[0] != face[-1]:
|
|
face2.append(face[0])
|
|
|
|
for i1, i2 in zip(face, face2):
|
|
coedge = self.make_coedge(i1, i2)
|
|
coedge.edge, coedge.sense = self.make_edge(i1, i2, coedge)
|
|
coedges.append(coedge)
|
|
loop = entities.Loop()
|
|
loop.set_coedges(coedges, close=True)
|
|
return loop
|
|
|
|
def make_coedge(self, index1: int, index2: int) -> entities.Coedge:
|
|
if index1 > index2:
|
|
key = index2, index1
|
|
else:
|
|
key = index1, index2
|
|
coedge = entities.Coedge()
|
|
try:
|
|
partner_coedge = self.partner_coedges[key]
|
|
except KeyError:
|
|
self.partner_coedges[key] = coedge
|
|
else:
|
|
partner_coedge.add_partner_coedge(coedge)
|
|
return coedge
|
|
|
|
def make_edge(
|
|
self, index1: int, index2: int, parent: entities.Coedge
|
|
) -> tuple[entities.Edge, bool]:
|
|
def make_vertex(index: int):
|
|
vertex = self.acis_vertices[index]
|
|
vertex.ref_count += 1
|
|
# assign first edge which references the vertex as parent edge (?):
|
|
if vertex.edge.is_none:
|
|
vertex.edge = edge
|
|
return vertex
|
|
|
|
sense = False
|
|
ex1 = index1 # vertex index of unified edges
|
|
ex2 = index2 # vertex index of unified edges
|
|
if ex1 > ex2:
|
|
sense = True
|
|
ex1, ex2 = ex2, ex1
|
|
try:
|
|
return self.edges[ex1, ex2], sense
|
|
except KeyError:
|
|
pass
|
|
# The edge has always the same direction as the underlying
|
|
# straight curve:
|
|
edge = entities.Edge()
|
|
edge.coedge = parent # first coedge which references this edge
|
|
edge.sense = False
|
|
edge.start_vertex = make_vertex(ex1)
|
|
edge.start_param = 0.0
|
|
edge.end_vertex = make_vertex(ex2)
|
|
edge.end_param = self.vertices[ex1].distance(self.vertices[ex2])
|
|
edge.curve = self.make_ray(ex1, ex2)
|
|
self.edges[ex1, ex2] = edge
|
|
return edge, sense
|
|
|
|
def make_ray(self, index1: int, index2: int) -> entities.StraightCurve:
|
|
v1 = self.vertices[index1]
|
|
v2 = self.vertices[index2]
|
|
ray = entities.StraightCurve()
|
|
ray.origin = v1
|
|
try:
|
|
ray.direction = (v2 - v1).normalize()
|
|
except ZeroDivisionError: # avoided by normalize_faces()
|
|
ray.direction = NULLVEC
|
|
return ray
|
|
|
|
|
|
def make_vertices(vertices: Iterable[Vec3]) -> Iterator[entities.Vertex]:
|
|
for v in vertices:
|
|
point = entities.Point()
|
|
point.location = v
|
|
vertex = entities.Vertex()
|
|
vertex.point = point
|
|
yield vertex
|
|
|
|
|
|
def vertices_from_body(body: Body) -> list[Vec3]:
|
|
"""Returns all stored vertices in the given :class:`Body` entity.
|
|
The result is not optimized, meaning the vertices are in no particular order and
|
|
there are duplicates.
|
|
|
|
This function can be useful to determining the approximate bounding box of an
|
|
:term:`ACIS` entity. The result is exact for polyhedra with flat faces with
|
|
straight edges, but not for bodies with curved edges and faces.
|
|
|
|
Args:
|
|
body: ACIS entity of type :class:`Body`
|
|
|
|
Raises:
|
|
TypeError: given `body` entity has invalid type
|
|
|
|
"""
|
|
|
|
if not isinstance(body, Body):
|
|
raise TypeError(f"expected a body entity, got: {type(body)}")
|
|
lump = body.lump
|
|
transform = body.transform
|
|
vertices: list[Vec3] = []
|
|
|
|
m: Optional[Matrix44] = None
|
|
if not transform.is_none:
|
|
m = transform.matrix
|
|
while not lump.is_none:
|
|
vertices.extend(vertices_from_lump(lump, m))
|
|
lump = lump.next_lump
|
|
return vertices
|
|
|
|
|
|
def vertices_from_lump(lump: Lump, m: Matrix44 | None = None) -> list[Vec3]:
|
|
"""Returns all stored vertices from a given :class:`Lump` entity.
|
|
Applies the transformation :class:`~ezdxf.math.Matrix44` `m` to all vertices if not
|
|
``None``.
|
|
|
|
Args:
|
|
lump: :class:`Lump` entity
|
|
m: optional transformation matrix
|
|
|
|
Raises:
|
|
TypeError: `lump` has invalid ACIS type
|
|
|
|
"""
|
|
if not isinstance(lump, Lump):
|
|
raise TypeError(f"expected a lump entity, got: {type(lump)}")
|
|
|
|
vertices: list[Vec3] = []
|
|
shell = lump.shell
|
|
if shell.is_none:
|
|
return vertices # not a shell
|
|
|
|
face = shell.face
|
|
while not face.is_none:
|
|
first_coedge = NONE_REF
|
|
try:
|
|
first_coedge = face.loop.coedge
|
|
except AttributeError: # loop is a none-entity
|
|
pass
|
|
coedge = first_coedge
|
|
while not coedge.is_none: # invalid coedge or face is not closed
|
|
# the edge entity contains the vertices and the curve type
|
|
edge = coedge.edge
|
|
try:
|
|
vertices.append(edge.start_vertex.point.location)
|
|
vertices.append(edge.end_vertex.point.location)
|
|
except AttributeError:
|
|
# one of the involved entities is a none-entity or an
|
|
# incompatible entity type -> ignore this face!
|
|
break
|
|
coedge = coedge.next_coedge
|
|
if coedge is first_coedge: # a valid closed face
|
|
break
|
|
face = face.next_face
|
|
if m is not None:
|
|
return list(m.transform_vertices(vertices))
|
|
return vertices
|