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

403 lines
14 KiB
Python

# Copyright (c) 2020-2023, Manfred Moitzi
# License: MIT License
from __future__ import annotations
import logging
from typing import (
TYPE_CHECKING,
Iterable,
Callable,
Optional,
cast,
)
from ezdxf.lldxf import const
from ezdxf.entities import factory
from ezdxf.entities.boundary_paths import (
PolylinePath,
EdgePath,
LineEdge,
ArcEdge,
EllipseEdge,
SplineEdge,
)
from ezdxf.math import OCS, Vec3, ABS_TOL
from ezdxf.math.transformtools import (
NonUniformScalingError,
InsertTransformationError,
)
from ezdxf.query import EntityQuery
from ezdxf.entities.copy import default_copy, CopyNotSupported
if TYPE_CHECKING:
from ezdxf.entities import (
DXFGraphic,
Insert,
Attrib,
Text,
Dimension,
)
from ezdxf.entities.polygon import DXFPolygon
from ezdxf.layouts import BaseLayout
logger = logging.getLogger("ezdxf")
__all__ = [
"virtual_block_reference_entities",
"virtual_boundary_path_entities",
"explode_block_reference",
"explode_entity",
"attrib_to_text",
]
def default_logging_callback(entity, reason):
logger.debug(
f'(Virtual Block Reference Entities) Ignoring {str(entity)}: "{reason}"'
)
def explode_block_reference(
block_ref: Insert,
target_layout: BaseLayout,
*,
redraw_order=False,
copy_strategy=default_copy,
) -> EntityQuery:
"""Explode a block reference into DXF primitives.
Transforms the block entities into the required WCS location by applying the
block reference attributes `insert`, `extrusion`, `rotation` and the scaling
values `xscale`, `yscale` and `zscale`.
Returns an EntityQuery() container with all exploded DXF entities.
Attached ATTRIB entities are converted to TEXT entities, this is the
behavior of the BURST command of the AutoCAD Express Tools.
This method does not apply the clipping path created by the XCLIP command.
The method returns all entities and ignores the clipping path polygon and no
entity is clipped.
Args:
block_ref: Block reference entity (INSERT)
target_layout: explicit target layout for exploded DXF entities
redraw_order: create entities in ascending redraw order if ``True``
copy_strategy: customizable copy strategy
.. warning::
**Non uniform scaling** may lead to incorrect results for text entities
(TEXT, MTEXT, ATTRIB) and maybe some other entities.
(internal API)
"""
if target_layout is None:
raise const.DXFStructureError("Target layout is None.")
if block_ref.doc is None:
raise const.DXFStructureError(
"Block reference has to be assigned to a DXF document."
)
def _explode_single_block_ref(block_ref):
for entity in virtual_block_reference_entities(
block_ref, redraw_order=redraw_order, copy_strategy=copy_strategy
):
dxftype = entity.dxftype()
target_layout.add_entity(entity)
if dxftype == "DIMENSION":
# Render a graphical representation for each exploded DIMENSION
# entity as anonymous block.
cast("Dimension", entity).render()
entities.append(entity)
# Convert attached ATTRIB entities to TEXT entities:
# This is the behavior of the BURST command of the AutoCAD Express Tools
for attrib in block_ref.attribs:
# Attached ATTRIB entities are already located in the WCS
text = attrib_to_text(attrib)
target_layout.add_entity(text)
entities.append(text)
entitydb = block_ref.doc.entitydb
assert (
entitydb is not None
), "Exploding a block reference requires an entity database."
entities: list[DXFGraphic] = []
if block_ref.mcount > 1:
for virtual_insert in block_ref.multi_insert():
_explode_single_block_ref(virtual_insert)
else:
_explode_single_block_ref(block_ref)
source_layout = block_ref.get_layout()
if source_layout is not None:
# Remove and destroy exploded INSERT if assigned to a layout
source_layout.delete_entity(block_ref)
else:
entitydb.delete_entity(block_ref)
return EntityQuery(entities)
IGNORE_FROM_ATTRIB = {
"version",
"prompt",
"tag",
"flags",
"field_length",
"lock_position",
"attribute_type",
}
def attrib_to_text(attrib: Attrib) -> Text:
dxfattribs = attrib.dxfattribs(drop=IGNORE_FROM_ATTRIB)
# ATTRIB has same owner as INSERT but does not reside in any EntitySpace()
# and must not deleted from any layout.
# New TEXT entity has same handle as the replaced ATTRIB entity and replaces
# the ATTRIB entity in the database.
text = factory.new("TEXT", dxfattribs=dxfattribs)
if attrib.doc:
factory.bind(text, attrib.doc)
return cast("Text", text)
def virtual_block_reference_entities(
block_ref: Insert,
*,
skipped_entity_callback: Optional[Callable[[DXFGraphic, str], None]] = None,
redraw_order=False,
copy_strategy=default_copy,
) -> Iterable[DXFGraphic]:
"""Yields 'virtual' parts of block reference `block_ref`.
This method is meant to examine the block reference entities without the need to
explode the block reference. The `skipped_entity_callback()` will be called for all
entities which are not processed, signature:
:code:`skipped_entity_callback(entity: DXFGraphic, reason: str)`,
`entity` is the original (untransformed) DXF entity of the block definition,
the `reason` string is an explanation why the entity was skipped.
These entities are located at the 'exploded' positions, but are not stored in
the entity database, have no handle and are not assigned to any layout.
This method does not apply the clipping path created by the XCLIP command.
The method returns all entities and ignores the clipping path polygon and no
entity is clipped.
Args:
block_ref: Block reference entity (INSERT)
skipped_entity_callback: called whenever the transformation of an entity
is not supported and so was skipped.
redraw_order: yield entities in ascending redraw order if ``True``
copy_strategy: customizable copy strategy
.. warning::
**Non uniform scaling** may lead to incorrect results for text entities
(TEXT, MTEXT, ATTRIB) and maybe some other entities.
(internal API)
"""
assert block_ref.dxftype() == "INSERT"
from ezdxf.entities import Ellipse
skipped_entity_callback = skipped_entity_callback or default_logging_callback
def disassemble(layout: BaseLayout) -> Iterable[DXFGraphic]:
for entity in layout.entities_in_redraw_order() if redraw_order else layout:
# Do not explode ATTDEF entities. Already available in Insert.attribs
if entity.dxftype() == "ATTDEF":
continue
try:
copy = entity.copy(copy_strategy=copy_strategy)
except CopyNotSupported:
if hasattr(entity, "virtual_entities"):
yield from entity.virtual_entities()
else:
skipped_entity_callback(entity, "non copyable")
else:
if hasattr(copy, "remove_association"):
copy.remove_association()
yield copy
def transform(entities):
for entity in entities:
try:
entity.transform(m)
except NotImplementedError:
skipped_entity_callback(entity, "non transformable")
except NonUniformScalingError:
dxftype = entity.dxftype()
if dxftype in {"ARC", "CIRCLE"}:
if abs(entity.dxf.radius) > ABS_TOL:
yield Ellipse.from_arc(entity).transform(m)
else:
skipped_entity_callback(
entity, f"Invalid radius in entity {str(entity)}."
)
elif dxftype in {"LWPOLYLINE", "POLYLINE"}: # has arcs
yield from transform(entity.virtual_entities())
else:
skipped_entity_callback(entity, "unsupported non-uniform scaling")
except InsertTransformationError:
# INSERT entity can not be represented in the target coordinate
# system defined by transformation matrix `m`.
# Yield transformed sub-entities of the INSERT entity:
yield from transform(
virtual_block_reference_entities(
entity, skipped_entity_callback=skipped_entity_callback
)
)
else:
yield entity
m = block_ref.matrix44()
block_layout = block_ref.block()
if block_layout is None:
raise const.DXFStructureError(
f'Required block definition for "{block_ref.dxf.name}" does not exist.'
)
yield from transform(disassemble(block_layout))
EXCLUDE_FROM_EXPLODE = {"POINT"}
def explode_entity(
entity: DXFGraphic, target_layout: Optional[BaseLayout] = None
) -> EntityQuery:
"""Explode parts of an entity as primitives into target layout, if target
layout is ``None``, the target layout is the layout of the source entity.
Returns an :class:`~ezdxf.query.EntityQuery` container with all DXF parts.
Args:
entity: DXF entity to explode, has to have a :meth:`virtual_entities()`
method
target_layout: target layout for DXF parts, ``None`` for same layout as
source entity
(internal API)
"""
dxftype = entity.dxftype()
virtual_entities = getattr(entity, "virtual_entities")
if virtual_entities is None or dxftype in EXCLUDE_FROM_EXPLODE:
raise const.DXFTypeError(f"Can not explode entity {dxftype}.")
if entity.doc is None:
raise const.DXFStructureError(
f"{dxftype} has to be assigned to a DXF document."
)
entitydb = entity.doc.entitydb
if entitydb is None:
raise const.DXFStructureError(
f"Exploding {dxftype} requires an entity database."
)
if target_layout is None:
target_layout = entity.get_layout()
if target_layout is None:
raise const.DXFStructureError(
f"{dxftype} without layout assignment, specify target layout."
)
entities = []
for e in virtual_entities():
target_layout.add_entity(e)
entities.append(e)
source_layout = entity.get_layout()
if source_layout is not None:
source_layout.delete_entity(entity)
else:
entitydb.delete_entity(entity)
return EntityQuery(entities)
def virtual_boundary_path_entities(
polygon: DXFPolygon,
) -> list[list[DXFGraphic]]:
from ezdxf.entities import LWPolyline
def polyline():
p = LWPolyline.new(dxfattribs=dict(graphic_attribs))
p.append_formatted_vertices(path.vertices, format="xyb")
p.dxf.extrusion = ocs.uz
p.dxf.elevation = elevation
p.closed = path.is_closed
return p
graphic_attribs = polygon.graphic_properties()
elevation = float(polygon.dxf.elevation.z)
ocs = polygon.ocs()
entities = []
for path in polygon.paths:
if isinstance(path, PolylinePath):
entities.append([polyline()])
elif isinstance(path, EdgePath):
entities.append(
_virtual_edge_path(path, dict(graphic_attribs), ocs, elevation)
)
return entities
def _virtual_edge_path(
path: EdgePath, dxfattribs, ocs: OCS, elevation: float
) -> list[DXFGraphic]:
from ezdxf.entities import Line, Arc, Ellipse, Spline
def pnt_to_wcs(v):
return ocs.to_wcs(Vec3(v).replace(z=elevation))
def dir_to_wcs(v):
return ocs.to_wcs(v)
edges: list[DXFGraphic] = []
for edge in path.edges:
attribs = dict(dxfattribs)
if isinstance(edge, LineEdge):
attribs["start"] = pnt_to_wcs(edge.start)
attribs["end"] = pnt_to_wcs(edge.end)
edges.append(Line.new(dxfattribs=attribs))
elif isinstance(edge, ArcEdge):
attribs["center"] = edge.center
attribs["radius"] = edge.radius
attribs["elevation"] = elevation
# Arcs angles are always stored in counter-clockwise orientation
# around the extrusion vector!
attribs["start_angle"] = edge.start_angle
attribs["end_angle"] = edge.end_angle
attribs["extrusion"] = ocs.uz
edges.append(Arc.new(dxfattribs=attribs))
elif isinstance(edge, EllipseEdge):
attribs["center"] = pnt_to_wcs(edge.center)
attribs["major_axis"] = dir_to_wcs(edge.major_axis)
attribs["ratio"] = edge.ratio
# Ellipse angles are always stored in counter-clockwise orientation
# around the extrusion vector!
attribs["start_param"] = edge.start_param
attribs["end_param"] = edge.end_param
attribs["extrusion"] = ocs.uz
edges.append(Ellipse.new(dxfattribs=attribs))
elif isinstance(edge, SplineEdge):
spline = Spline.new(dxfattribs=attribs)
spline.dxf.degree = edge.degree
spline.knots = edge.knot_values
spline.control_points = [pnt_to_wcs(v) for v in edge.control_points]
if edge.weights:
spline.weights = edge.weights
if edge.fit_points:
spline.fit_points = [pnt_to_wcs(v) for v in edge.fit_points]
if edge.start_tangent is not None:
spline.dxf.start_tangent = dir_to_wcs(edge.start_tangent)
if edge.end_tangent is not None:
spline.dxf.end_tangent = dir_to_wcs(edge.end_tangent)
edges.append(spline)
return edges