This commit is contained in:
Christian Anetzberger
2026-01-22 20:23:51 +01:00
commit a197de9456
4327 changed files with 1235205 additions and 0 deletions

View File

@@ -0,0 +1,320 @@
# Copyright (c) 2019-2022 Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import TYPE_CHECKING, Iterable, Optional
import math
from ezdxf.audit import AuditError
from ezdxf.lldxf import validator
from ezdxf.math import (
Vec3,
Matrix44,
NULLVEC,
X_AXIS,
Z_AXIS,
ellipse,
ConstructionEllipse,
OCS,
)
from ezdxf.lldxf.attributes import (
DXFAttr,
DXFAttributes,
DefSubclass,
XType,
RETURN_DEFAULT,
group_code_mapping,
merge_group_code_mappings,
)
from ezdxf.lldxf.const import SUBCLASS_MARKER, DXF2000
from .dxfentity import base_class, SubclassProcessor
from .dxfgfx import (
DXFGraphic,
acdb_entity,
add_entity,
replace_entity,
acdb_entity_group_codes,
)
from .factory import register_entity
if TYPE_CHECKING:
from ezdxf.lldxf.tagwriter import AbstractTagWriter
from ezdxf.entities import DXFNamespace, Spline
from ezdxf.audit import Auditor
__all__ = ["Ellipse"]
MIN_RATIO = 1e-10 # tested with DWG TrueView 2022
MAX_RATIO = 1.0 # tested with DWG TrueView 2022
TOL = 1e-9 # arbitrary choice
def is_valid_ratio(ratio: float) -> bool:
"""Check if axis-ratio is in valid range, takes an upper bound tolerance into
account for floating point imprecision.
"""
return MIN_RATIO <= abs(ratio) < (MAX_RATIO + TOL)
def clamp_axis_ratio(ratio: float) -> float:
"""Clamp axis-ratio into valid range and remove possible floating point imprecision.
"""
sign = -1 if ratio < 0 else +1
ratio = abs(ratio)
if ratio < MIN_RATIO:
return MIN_RATIO * sign
if ratio > MAX_RATIO:
return MAX_RATIO * sign
return ratio * sign
acdb_ellipse = DefSubclass(
"AcDbEllipse",
{
"center": DXFAttr(10, xtype=XType.point3d, default=NULLVEC),
# Major axis vector from 'center':
"major_axis": DXFAttr(
11,
xtype=XType.point3d,
default=X_AXIS,
validator=validator.is_not_null_vector,
),
# The extrusion vector does not establish an OCS, it is just the normal
# vector of the ellipse plane:
"extrusion": DXFAttr(
210,
xtype=XType.point3d,
default=Z_AXIS,
optional=True,
validator=validator.is_not_null_vector,
fixer=RETURN_DEFAULT,
),
# Ratio has to be in the range: -1.0 ... -1e-10 and +1e-10 ... +1.0:
"ratio": DXFAttr(
40, default=MAX_RATIO, validator=is_valid_ratio, fixer=clamp_axis_ratio
),
# Start of ellipse, this value is 0.0 for a full ellipse:
"start_param": DXFAttr(41, default=0),
# End of ellipse, this value is 2π for a full ellipse:
"end_param": DXFAttr(42, default=math.tau),
},
)
acdb_ellipse_group_code = group_code_mapping(acdb_ellipse)
merged_ellipse_group_codes = merge_group_code_mappings(
acdb_entity_group_codes, acdb_ellipse_group_code # type: ignore
)
HALF_PI = math.pi / 2.0
@register_entity
class Ellipse(DXFGraphic):
"""DXF ELLIPSE entity"""
DXFTYPE = "ELLIPSE"
DXFATTRIBS = DXFAttributes(base_class, acdb_entity, acdb_ellipse)
MIN_DXF_VERSION_FOR_EXPORT = DXF2000
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_ellipse_group_codes)
return dxf
def export_entity(self, tagwriter: AbstractTagWriter) -> None:
"""Export entity specific data as DXF tags."""
super().export_entity(tagwriter)
tagwriter.write_tag2(SUBCLASS_MARKER, acdb_ellipse.name)
# is_valid_ratio() takes floating point imprecision on the upper bound of
# +/- 1.0 into account:
assert is_valid_ratio(
self.dxf.ratio
), f"axis-ratio out of range [{MIN_RATIO}, {MAX_RATIO}]"
self.dxf.ratio = clamp_axis_ratio(self.dxf.ratio)
self.dxf.export_dxf_attribs(
tagwriter,
[
"center",
"major_axis",
"extrusion",
"ratio",
"start_param",
"end_param",
],
)
@property
def minor_axis(self) -> Vec3:
dxf = self.dxf
return ellipse.minor_axis(Vec3(dxf.major_axis), Vec3(dxf.extrusion), dxf.ratio)
@property
def start_point(self) -> Vec3:
return list(self.vertices([self.dxf.start_param]))[0]
@property
def end_point(self) -> Vec3:
return list(self.vertices([self.dxf.end_param]))[0]
def construction_tool(self) -> ConstructionEllipse:
"""Returns construction tool :class:`ezdxf.math.ConstructionEllipse`."""
dxf = self.dxf
return ConstructionEllipse(
dxf.center,
dxf.major_axis,
dxf.extrusion,
dxf.ratio,
dxf.start_param,
dxf.end_param,
)
def apply_construction_tool(self, e: ConstructionEllipse) -> Ellipse:
"""Set ELLIPSE data from construction tool
:class:`ezdxf.math.ConstructionEllipse`.
"""
self.update_dxf_attribs(e.dxfattribs())
return self # floating interface
def params(self, num: int) -> Iterable[float]:
"""Returns `num` params from start- to end param in counter-clockwise
order.
All params are normalized in the range [0, 2π).
"""
start = self.dxf.start_param % math.tau
end = self.dxf.end_param % math.tau
yield from ellipse.get_params(start, end, num)
def vertices(self, params: Iterable[float]) -> Iterable[Vec3]:
"""Yields vertices on ellipse for iterable `params` in WCS.
Args:
params: param values in the range from 0 to 2π in radians,
param goes counter-clockwise around the extrusion vector,
major_axis = local x-axis = 0 rad.
"""
yield from self.construction_tool().vertices(params)
def flattening(self, distance: float, segments: int = 8) -> Iterable[Vec3]:
"""Adaptive recursive flattening. The argument `segments` is the
minimum count of approximation segments, if the distance from the center
of the approximation segment to the curve is bigger than `distance` the
segment will be subdivided. Returns a closed polygon for a full ellipse
where the start vertex has the same value as the end vertex.
Args:
distance: maximum distance from the projected curve point onto the
segment chord.
segments: minimum segment count
"""
return self.construction_tool().flattening(distance, segments)
def swap_axis(self):
"""Swap axis and adjust start- and end parameter."""
e = self.construction_tool()
e.swap_axis()
self.update_dxf_attribs(e.dxfattribs())
@classmethod
def from_arc(cls, entity: DXFGraphic) -> Ellipse:
"""Create a new virtual ELLIPSE entity from an ARC or a CIRCLE entity.
The new entity has no owner, no handle, is not stored in the entity database nor
assigned to any layout!
"""
assert entity.dxftype() in {"ARC", "CIRCLE"}, "ARC or CIRCLE entity required"
attribs = entity.dxfattribs(drop={"owner", "handle", "thickness"})
e = ellipse.ConstructionEllipse.from_arc(
center=attribs.get("center", NULLVEC),
extrusion=attribs.get("extrusion", Z_AXIS),
# Remove all not ELLIPSE attributes:
radius=attribs.pop("radius", 1.0),
start_angle=attribs.pop("start_angle", 0),
end_angle=attribs.pop("end_angle", 360),
)
attribs.update(e.dxfattribs())
return cls.new(dxfattribs=attribs, doc=entity.doc)
def transform(self, m: Matrix44) -> Ellipse:
"""Transform the ELLIPSE entity by transformation matrix `m` inplace."""
e = self.construction_tool()
e.transform(m)
self.update_dxf_attribs(e.dxfattribs())
self.post_transform(m)
return self
def translate(self, dx: float, dy: float, dz: float) -> Ellipse:
"""Optimized ELLIPSE translation about `dx` in x-axis, `dy` in y-axis
and `dz` in z-axis, returns `self` (floating interface).
"""
self.dxf.center = Vec3(dx, dy, dz) + self.dxf.center
# Avoid Matrix44 instantiation if not required:
if self.is_post_transform_required:
self.post_transform(Matrix44.translate(dx, dy, dz))
return self
def to_spline(self, replace=True) -> Spline:
"""Convert ELLIPSE to a :class:`~ezdxf.entities.Spline` entity.
Adds the new SPLINE entity to the entity database and to the
same layout as the source entity.
Args:
replace: replace (delete) source entity by SPLINE entity if ``True``
"""
from ezdxf.entities import Spline
spline = Spline.from_arc(self)
layout = self.get_layout()
assert layout is not None, "valid layout required"
if replace:
replace_entity(self, spline, layout)
else:
add_entity(spline, layout)
return spline
def ocs(self) -> OCS:
# WCS entity which supports the "extrusion" attribute in a
# different way!
return OCS()
def audit(self, auditor: Auditor) -> None:
if not self.is_alive:
return
super().audit(auditor)
entity = str(self)
major_axis = Vec3(self.dxf.major_axis)
if major_axis.is_null:
auditor.trash(self)
auditor.fixed_error(
code=AuditError.INVALID_MAJOR_AXIS,
message=f"Removed {entity} with invalid major axis: (0, 0, 0).",
)
return
axis_ratio = self.dxf.ratio
if is_valid_ratio(axis_ratio):
# remove possible floating point imprecision:
self.dxf.ratio = clamp_axis_ratio(axis_ratio)
return
if abs(axis_ratio) > MAX_RATIO:
self.swap_axis()
auditor.fixed_error(
code=AuditError.INVALID_ELLIPSE_RATIO,
message=f"Fixed invalid axis-ratio in {entity} by swapping axis.",
)
elif abs(axis_ratio) < MIN_RATIO:
self.dxf.ratio = clamp_axis_ratio(axis_ratio)
auditor.fixed_error(
code=AuditError.INVALID_ELLIPSE_RATIO,
message=f"Fixed invalid axis-ratio in {entity}, set to {MIN_RATIO}.",
)