initial
This commit is contained in:
320
.venv/lib/python3.12/site-packages/ezdxf/entities/ellipse.py
Normal file
320
.venv/lib/python3.12/site-packages/ezdxf/entities/ellipse.py
Normal 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}.",
|
||||
)
|
||||
Reference in New Issue
Block a user