initial
This commit is contained in:
398
.venv/lib/python3.12/site-packages/ezdxf/transform.py
Normal file
398
.venv/lib/python3.12/site-packages/ezdxf/transform.py
Normal file
@@ -0,0 +1,398 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user