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

280 lines
10 KiB
Python

# Copyright (c) 2019-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Optional
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.math import Matrix44, Z_AXIS, NULLVEC, Vec3
from ezdxf.math.transformtools import OCSTransform
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import (
DXFGraphic,
acdb_entity,
elevation_to_z_axis,
acdb_entity_group_codes,
)
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entities import DXFNamespace
__all__ = ["Solid", "Trace", "Face3d"]
acdb_trace = DefSubclass(
"AcDbTrace",
{
# IMPORTANT: all 4 vertices have to be present in the DXF file,
# otherwise AutoCAD shows a DXF structure error and does not load the
# file! (SOLID, TRACE and 3DFACE)
# 1. corner Solid WCS; Trace OCS
"vtx0": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# 2. corner Solid WCS; Trace OCS
"vtx1": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
# 3. corner Solid WCS; Trace OCS
"vtx2": DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
# 4. corner Solid WCS; Trace OCS:
# If only three corners are entered to define the SOLID, then the fourth
# corner coordinate is the same as the third.
"vtx3": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
# IMPORTANT: for TRACE and SOLID the last two vertices are in reversed
# order: a square has the vertex order 0-1-3-2
# Elevation is a legacy feature from R11 and prior, do not use this
# attribute, store the entity elevation in the z-axis of the vertices.
# ezdxf does not export the elevation attribute!
"elevation": DXFAttr(38, default=0, optional=True),
# Thickness could be negative:
"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_trace_group_codes = group_code_mapping(acdb_trace)
merged_trace_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_trace_group_codes # type: ignore
)
class _Base(DXFGraphic):
def __getitem__(self, num):
return self.dxf.get(VERTEXNAMES[num])
def __setitem__(self, num, value):
return self.dxf.set(VERTEXNAMES[num], value)
@register_entity
class Solid(_Base):
"""DXF SHAPE entity"""
DXFTYPE = "SOLID"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_trace)
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_trace_group_codes)
if processor.r12:
# Transform elevation attribute from R11 to z-axis values:
elevation_to_z_axis(dxf, VERTEXNAMES)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags. (internal API)"""
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_trace.name)
if not self.dxf.hasattr("vtx3"):
self.dxf.vtx3 = self.dxf.vtx2
self.dxf.export_dxf_attribs(
tagwriter,
[
"vtx0",
"vtx1",
"vtx2",
"vtx3",
"thickness",
"extrusion",
],
)
def transform(self, m: Matrix44) -> Solid:
"""Transform the SOLID/TRACE entity by transformation matrix `m` inplace."""
# SOLID and TRACE are OCS entities.
dxf = self.dxf
ocs = OCSTransform(self.dxf.extrusion, m)
for name in VERTEXNAMES:
if dxf.hasattr(name):
dxf.set(name, ocs.transform_vertex(dxf.get(name)))
if dxf.hasattr("thickness"):
dxf.thickness = ocs.transform_thickness(dxf.thickness)
dxf.extrusion = ocs.new_extrusion
self.post_transform(m)
return self
def wcs_vertices(self, close: bool = False) -> list[Vec3]:
"""Returns WCS vertices in correct order,
if argument `close` is ``True``, last vertex == first vertex.
Does **not** return the duplicated last vertex if the entity represents
a triangle.
"""
ocs = self.ocs()
return list(ocs.points_to_wcs(self.vertices(close)))
def vertices(self, close: bool = False) -> list[Vec3]:
"""Returns OCS vertices in correct order,
if argument `close` is ``True``, last vertex == first vertex.
Does **not** return the duplicated last vertex if the entity represents
a triangle.
"""
dxf = self.dxf
vertices: list[Vec3] = [dxf.vtx0, dxf.vtx1, dxf.vtx2]
if dxf.vtx3 != dxf.vtx2: # face is not a triangle
vertices.append(dxf.vtx3)
# adjust weird vertex order of SOLID and TRACE:
# 0, 1, 2, 3 -> 0, 1, 3, 2
if len(vertices) > 3:
vertices[2], vertices[3] = vertices[3], vertices[2]
if close and not vertices[0].isclose(vertices[-1]):
vertices.append(vertices[0])
return vertices
@register_entity
class Trace(Solid):
"""DXF TRACE entity"""
DXFTYPE = "TRACE"
acdb_face = DefSubclass(
"AcDbFace",
{
# 1. corner WCS:
"vtx0": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# 2. corner WCS:
"vtx1": DXFAttr(11, xtype=XType.point3d, default=NULLVEC),
# 3. corner WCS:
"vtx2": DXFAttr(12, xtype=XType.point3d, default=NULLVEC),
# 4. corner WCS:
# If only three corners are entered to define the SOLID, then the fourth
# corner coordinate is the same as the third.
"vtx3": DXFAttr(13, xtype=XType.point3d, default=NULLVEC),
# invisible:
# 1 = First edge is invisible
# 2 = Second edge is invisible
# 4 = Third edge is invisible
# 8 = Fourth edge is invisible
"invisible_edges": DXFAttr(70, default=0, optional=True),
},
)
acdb_face_group_codes = group_code_mapping(acdb_face)
merged_face_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_face_group_codes # type: ignore
)
@register_entity
class Face3d(_Base):
"""DXF 3DFACE entity"""
# IMPORTANT: for 3DFACE the last two vertices are in regular order:
# a square has the vertex order 0-1-2-3
DXFTYPE = "3DFACE"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_face)
def is_invisible_edge(self, num: int) -> bool:
"""Returns True if edge `num` is an invisible edge."""
if num < 0 or num > 4:
raise ValueError(f"invalid edge: {num}")
return bool(self.dxf.invisible_edges & (1 << num))
def set_edge_visibility(self, num: int, visible: bool = False) -> None:
"""Set visibility of edge `num`, status `True` for visible, status
`False` for invisible.
"""
if num < 0 or num >= 4:
raise ValueError(f"invalid edge: {num}")
if not visible:
self.dxf.invisible_edges = self.dxf.invisible_edges | (1 << num)
else:
self.dxf.invisible_edges = self.dxf.invisible_edges & ~(1 << num)
def get_edges_visibility(self) -> list[bool]:
# if the face is a triangle, a fourth visibility flag
# may be present but is ignored
return [not self.is_invisible_edge(i) for i in range(4)]
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_face_group_codes)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
super().export_entity(tagwriter)
if tagwriter.dxfversion > DXF12:
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_face.name)
if not self.dxf.hasattr("vtx3"):
self.dxf.vtx3 = self.dxf.vtx2
self.dxf.export_dxf_attribs(
tagwriter, ["vtx0", "vtx1", "vtx2", "vtx3", "invisible_edges"]
)
def transform(self, m: Matrix44) -> Face3d:
"""Transform the 3DFACE entity by transformation matrix `m` inplace."""
dxf = self.dxf
# 3DFACE is a real 3d entity
dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3 = m.transform_vertices(
(dxf.vtx0, dxf.vtx1, dxf.vtx2, dxf.vtx3)
)
self.post_transform(m)
return self
def wcs_vertices(self, close: bool = False) -> list[Vec3]:
"""Returns WCS vertices, if argument `close` is ``True``,
the first vertex is also returned as closing last vertex.
Returns 4 vertices when `close` is ``False`` and 5 vertices when `close` is
``True``. Some edges may have zero-length. This is a compatibility interface
to SOLID and TRACE. The 3DFACE entity is already defined by WCS vertices.
"""
dxf = self.dxf
vertices: list[Vec3] = [dxf.vtx0, dxf.vtx1, dxf.vtx2]
vtx3 = dxf.get("vtx3")
if (
isinstance(vtx3, Vec3) and vtx3 != dxf.vtx2
): # face is not a triangle
vertices.append(vtx3)
if close:
vertices.append(vertices[0])
return vertices