399 lines
12 KiB
Python
399 lines
12 KiB
Python
# Copyright (c) 2023-2024, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import (
|
|
Iterable,
|
|
Iterator,
|
|
NamedTuple,
|
|
Optional,
|
|
Tuple,
|
|
List,
|
|
TYPE_CHECKING,
|
|
Sequence,
|
|
)
|
|
import enum
|
|
import logging
|
|
|
|
from ezdxf.math import (
|
|
Matrix44,
|
|
UVec,
|
|
Vec3,
|
|
NonUniformScalingError,
|
|
InsertTransformationError,
|
|
)
|
|
from ezdxf.entities import (
|
|
DXFEntity,
|
|
DXFGraphic,
|
|
Circle,
|
|
LWPolyline,
|
|
Polyline,
|
|
Ellipse,
|
|
is_graphic_entity,
|
|
)
|
|
from ezdxf.entities.copy import default_copy, CopyNotSupported
|
|
from ezdxf.protocols import SupportsTemporaryTransformation
|
|
from ezdxf.document import Drawing
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.layouts import BlockLayout
|
|
|
|
__all__ = [
|
|
"Logger",
|
|
"Error",
|
|
"inplace",
|
|
"copies",
|
|
"translate",
|
|
"scale_uniform",
|
|
"scale",
|
|
"x_rotate",
|
|
"y_rotate",
|
|
"z_rotate",
|
|
"axis_rotate",
|
|
"transform_entity_by_blockref",
|
|
"transform_entities_by_blockref",
|
|
]
|
|
MIN_SCALING_FACTOR = 1e-12
|
|
logger = logging.getLogger("ezdxf")
|
|
|
|
|
|
class Error(enum.Enum):
|
|
NONE = 0
|
|
TRANSFORMATION_NOT_SUPPORTED = enum.auto()
|
|
COPY_NOT_SUPPORTED = enum.auto()
|
|
NON_UNIFORM_SCALING_ERROR = enum.auto()
|
|
INSERT_TRANSFORMATION_ERROR = enum.auto()
|
|
VIRTUAL_ENTITY_NOT_SUPPORTED = enum.auto()
|
|
|
|
|
|
class Logger:
|
|
class Entry(NamedTuple):
|
|
error: Error
|
|
message: str
|
|
entity: DXFEntity
|
|
|
|
def __init__(self) -> None:
|
|
self._entries: list[Logger.Entry] = []
|
|
|
|
def __getitem__(self, index: int) -> Entry:
|
|
"""Returns the error entry at `index`."""
|
|
return self._entries[index]
|
|
|
|
def __iter__(self) -> Iterator[Entry]:
|
|
"""Iterates over all error entries."""
|
|
return iter(self._entries)
|
|
|
|
def __len__(self) -> int:
|
|
"""Returns the count of error entries."""
|
|
return len(self._entries)
|
|
|
|
def add(self, error: Error, message: str, entity: DXFEntity):
|
|
self._entries.append(Logger.Entry(error, message, entity))
|
|
|
|
def messages(self) -> list[str]:
|
|
"""Returns all error messages as list of strings."""
|
|
return [entry.message for entry in self._entries]
|
|
|
|
def purge(self, entries: Iterable[Entry]) -> None:
|
|
for entry in entries:
|
|
try:
|
|
self._entries.remove(entry)
|
|
except ValueError:
|
|
pass
|
|
|
|
|
|
def _inplace(entities: Iterable[DXFEntity], m: Matrix44) -> Logger:
|
|
"""Transforms the given `entities` inplace by the transformation matrix `m`,
|
|
non-uniform scaling is not supported. The function logs errors and does not raise
|
|
errors for unsupported entities or transformations that cannot be performed,
|
|
see enum :class:`Error`.
|
|
The :func:`inplace` function supports virtual entities as well.
|
|
|
|
"""
|
|
log = Logger()
|
|
for entity in entities:
|
|
if not entity.is_alive:
|
|
continue
|
|
try:
|
|
entity.transform(m) # type: ignore
|
|
except (AttributeError, NotImplementedError):
|
|
log.add(
|
|
Error.TRANSFORMATION_NOT_SUPPORTED,
|
|
f"{str(entity)} entity does not support transformation",
|
|
entity,
|
|
)
|
|
except NonUniformScalingError:
|
|
log.add(
|
|
Error.NON_UNIFORM_SCALING_ERROR,
|
|
f"{str(entity)} entity does not support non-uniform scaling",
|
|
entity,
|
|
)
|
|
except InsertTransformationError:
|
|
log.add(
|
|
Error.INSERT_TRANSFORMATION_ERROR,
|
|
f"{str(entity)} entity can not represent a non-orthogonal target coordinate system",
|
|
entity,
|
|
)
|
|
|
|
return log
|
|
|
|
|
|
def inplace(
|
|
entities: Iterable[DXFEntity],
|
|
m: Matrix44,
|
|
) -> Logger:
|
|
"""Transforms the given `entities` inplace by the transformation matrix `m`,
|
|
non-uniform scaling is supported. The function converts circular arcs into ellipses
|
|
to perform non-uniform scaling. The function logs errors and does not raise errors
|
|
for unsupported entities or transformation errors, see enum :class:`Error`.
|
|
|
|
.. important::
|
|
|
|
The :func:`inplace` function does not support type conversion for virtual
|
|
entities e.g. non-uniform scaling for CIRCLE, ARC or POLYLINE with bulges,
|
|
see also function :func:`copies`.
|
|
|
|
"""
|
|
log = _inplace(entities, m)
|
|
errors: list[Logger.Entry] = []
|
|
for entry in log:
|
|
if entry.error != Error.NON_UNIFORM_SCALING_ERROR:
|
|
continue
|
|
|
|
entity = entry.entity
|
|
if entity.is_virtual:
|
|
errors.append(entry)
|
|
log.add(
|
|
Error.VIRTUAL_ENTITY_NOT_SUPPORTED,
|
|
f"non-uniform scaling is not supported for virtual entity {str(entity)}",
|
|
entity,
|
|
)
|
|
continue
|
|
|
|
if isinstance(entity, Circle): # CIRCLE, ARC
|
|
errors.append(entry)
|
|
ellipse = entity.to_ellipse(replace=True)
|
|
ellipse.transform(m)
|
|
elif isinstance(entity, (LWPolyline, Polyline)): # has bulges (circular arcs)
|
|
errors.append(entry)
|
|
for sub_entity in entity.explode():
|
|
if isinstance(sub_entity, Circle):
|
|
sub_entity = sub_entity.to_ellipse()
|
|
sub_entity.transform(m) # type: ignore
|
|
# else: NON_UNIFORM_SCALING_ERROR stays unchanged
|
|
log.purge(errors)
|
|
return log
|
|
|
|
|
|
def copies(
|
|
entities: Iterable[DXFEntity], m: Optional[Matrix44] = None
|
|
) -> Tuple[Logger, List[DXFEntity]]:
|
|
"""Copy entities and transform them by matrix `m`. Does not raise any exception
|
|
and ignores all entities that cannot be copied or transformed. Just copies the input
|
|
entities if matrix `m` is ``None``. Returns a tuple of :class:`Logger` and a list of
|
|
transformed virtual copies. The function supports virtual entities as input and
|
|
converts circular arcs into ellipses to perform non-uniform scaling.
|
|
"""
|
|
|
|
log = Logger()
|
|
clones = _copy_entities(entities, log)
|
|
if isinstance(m, Matrix44):
|
|
clones = _transform_clones(clones, m, log)
|
|
return log, clones
|
|
|
|
|
|
def _copy_entities(entities: Iterable[DXFEntity], log: Logger) -> list[DXFEntity]:
|
|
clones: list[DXFEntity] = []
|
|
for entity in entities:
|
|
if not entity.is_alive:
|
|
continue
|
|
try:
|
|
clone = entity.copy(copy_strategy=default_copy)
|
|
except CopyNotSupported:
|
|
log.add(
|
|
Error.COPY_NOT_SUPPORTED,
|
|
f"{str(entity)} entity does not support copy",
|
|
entity,
|
|
)
|
|
else:
|
|
clones.append(clone)
|
|
return clones
|
|
|
|
|
|
def _transform_clones(clones: Iterable[DXFEntity], m: Matrix44, log: Logger):
|
|
entities: List[DXFEntity] = []
|
|
for entity in clones:
|
|
try:
|
|
entity.transform(m) # type: ignore
|
|
except (AttributeError, NotImplementedError):
|
|
log.add(
|
|
Error.TRANSFORMATION_NOT_SUPPORTED,
|
|
f"{str(entity)} entity does not support transformation",
|
|
entity,
|
|
)
|
|
except InsertTransformationError:
|
|
log.add(
|
|
Error.INSERT_TRANSFORMATION_ERROR,
|
|
f"{str(entity)} entity can not represent a non-orthogonal target coordinate system",
|
|
entity,
|
|
)
|
|
except NonUniformScalingError:
|
|
try:
|
|
entities.extend(_scale_non_uniform(entity, m))
|
|
except TypeError:
|
|
log.add(
|
|
Error.NON_UNIFORM_SCALING_ERROR,
|
|
f"{str(entity)} entity does not support non-uniform scaling",
|
|
entity,
|
|
)
|
|
else:
|
|
entities.append(entity)
|
|
|
|
return entities
|
|
|
|
|
|
def _scale_non_uniform(entity: DXFEntity, m: Matrix44):
|
|
sub_entity: DXFGraphic
|
|
if isinstance(entity, Circle):
|
|
sub_entity = Ellipse.from_arc(entity)
|
|
sub_entity.transform(m)
|
|
yield sub_entity
|
|
elif isinstance(entity, (LWPolyline, Polyline)):
|
|
for sub_entity in entity.virtual_entities():
|
|
if isinstance(sub_entity, Circle):
|
|
sub_entity = Ellipse.from_arc(sub_entity)
|
|
sub_entity.transform(m)
|
|
yield sub_entity
|
|
else:
|
|
raise TypeError
|
|
|
|
|
|
def translate(entities: Iterable[DXFEntity], offset: UVec) -> Logger:
|
|
"""Translates (moves) `entities` inplace by the `offset` vector."""
|
|
v = Vec3(offset)
|
|
if v:
|
|
return _inplace(entities, m=Matrix44.translate(v.x, v.y, v.z))
|
|
return Logger()
|
|
|
|
|
|
def scale_uniform(entities: Iterable[DXFEntity], factor: float) -> Logger:
|
|
"""Scales `entities` inplace by a `factor` in all axis. Scaling factors smaller than
|
|
:attr:`MIN_SCALING_FACTOR` are ignored.
|
|
|
|
"""
|
|
f = float(factor)
|
|
if abs(f) > MIN_SCALING_FACTOR:
|
|
return _inplace(entities, m=Matrix44.scale(f, f, f))
|
|
return Logger()
|
|
|
|
|
|
def scale(entities: Iterable[DXFEntity], sx: float, sy: float, sz: float) -> Logger:
|
|
"""Scales `entities` inplace by the factors `sx` in x-axis, `sy` in y-axis and `sz`
|
|
in z-axis. Scaling factors smaller than :attr:`MIN_SCALING_FACTOR` are ignored.
|
|
|
|
.. important::
|
|
|
|
same limitations for virtual entities as the :func:`inplace` function
|
|
|
|
"""
|
|
|
|
def safe(f: float) -> float:
|
|
f = float(f)
|
|
return f if abs(f) > MIN_SCALING_FACTOR else 1.0
|
|
|
|
return inplace(entities, Matrix44.scale(safe(sx), safe(sy), safe(sz)))
|
|
|
|
|
|
def x_rotate(entities: Iterable[DXFEntity], angle: float) -> Logger:
|
|
"""Rotates `entities` inplace by `angle` in radians about the x-axis."""
|
|
a = float(angle)
|
|
if a:
|
|
return _inplace(entities, m=Matrix44.x_rotate(a))
|
|
return Logger()
|
|
|
|
|
|
def y_rotate(entities: Iterable[DXFEntity], angle: float) -> Logger:
|
|
"""Rotates `entities` inplace by `angle` in radians about the y-axis."""
|
|
a = float(angle)
|
|
if a:
|
|
return _inplace(entities, m=Matrix44.y_rotate(a))
|
|
return Logger()
|
|
|
|
|
|
def z_rotate(entities: Iterable[DXFEntity], angle: float) -> Logger:
|
|
"""Rotates `entities` inplace by `angle` in radians about the x-axis."""
|
|
a = float(angle)
|
|
if a:
|
|
return _inplace(entities, m=Matrix44.z_rotate(a))
|
|
return Logger()
|
|
|
|
|
|
def axis_rotate(entities: Iterable[DXFEntity], axis: UVec, angle: float) -> Logger:
|
|
"""Rotates `entities` inplace by `angle` in radians about the rotation axis starting
|
|
at the origin pointing in `axis` direction.
|
|
"""
|
|
a = float(angle)
|
|
if not a:
|
|
return Logger()
|
|
|
|
v = Vec3(axis)
|
|
if not v.is_null:
|
|
return _inplace(entities, m=Matrix44.axis_rotate(v, a))
|
|
return Logger()
|
|
|
|
|
|
def transform_entity_by_blockref(entity: DXFEntity, m: Matrix44) -> bool:
|
|
"""Apply a transformation by moving an entity into a block and replacing the entity
|
|
by a block reference with the applied transformation.
|
|
"""
|
|
return _transform_by_blockref([entity], m) is not None
|
|
|
|
|
|
def transform_entities_by_blockref(
|
|
entities: Iterable[DXFEntity], m: Matrix44
|
|
) -> BlockLayout | None:
|
|
"""Apply a transformation by moving entities into a block and replacing the entities
|
|
by a block reference with the applied transformation.
|
|
"""
|
|
return _transform_by_blockref(list(entities), m)
|
|
|
|
|
|
def _transform_by_blockref(
|
|
entities: Sequence[DXFEntity], m: Matrix44
|
|
) -> BlockLayout | None:
|
|
if len(entities) == 0:
|
|
return None
|
|
|
|
first_entity = entities[0]
|
|
if not is_graphic_entity(first_entity):
|
|
return None
|
|
|
|
doc = first_entity.doc
|
|
if doc is None:
|
|
return None
|
|
|
|
layout = first_entity.get_layout()
|
|
if layout is None:
|
|
return None
|
|
|
|
block = doc.blocks.new_anonymous_block()
|
|
insert = layout.add_blockref(block.name, (0, 0, 0))
|
|
try:
|
|
insert.transform(m)
|
|
except InsertTransformationError:
|
|
logger.warning(f"cannot apply invalid transformation")
|
|
layout.delete_entity(insert)
|
|
doc.blocks.delete_block(block.name, safe=False)
|
|
return None
|
|
|
|
for e in entities:
|
|
if is_graphic_entity(e):
|
|
layout.move_to_layout(e, block)
|
|
return block
|
|
|
|
|
|
def apply_temporary_transformations(entities: Iterable[DXFEntity]) -> None:
|
|
for entity in entities:
|
|
if isinstance(entity, SupportsTemporaryTransformation):
|
|
tt = entity.temporary_transformation()
|
|
tt.apply_transformation(entity)
|