refactor: excel parse
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
# Copyright (c) 2010-2024 Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
Generic,
|
||||
)
|
||||
import math
|
||||
|
||||
# The pure Python implementation can't import from ._ctypes or ezdxf.math!
|
||||
from ._vector import Vec3, Vec2
|
||||
from ._matrix44 import Matrix44
|
||||
from ._construct import arc_angle_span_deg
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ezdxf.math import UVec
|
||||
from ezdxf.math.ellipse import ConstructionEllipse
|
||||
|
||||
__all__ = [
|
||||
"Bezier4P",
|
||||
"cubic_bezier_arc_parameters",
|
||||
"cubic_bezier_from_arc",
|
||||
"cubic_bezier_from_ellipse",
|
||||
]
|
||||
|
||||
T = TypeVar("T", Vec2, Vec3)
|
||||
|
||||
|
||||
class Bezier4P(Generic[T]):
|
||||
"""Implements an optimized cubic `Bézier curve`_ for exact 4 control points.
|
||||
|
||||
A `Bézier curve`_ is a parametric curve, parameter `t` goes from 0 to 1,
|
||||
where 0 is the first control point and 1 is the fourth control point.
|
||||
|
||||
The class supports points of type :class:`Vec2` and :class:`Vec3` as input, the
|
||||
class instances are immutable.
|
||||
|
||||
Args:
|
||||
defpoints: sequence of definition points as :class:`Vec2` or
|
||||
:class:`Vec3` compatible objects.
|
||||
|
||||
"""
|
||||
|
||||
__slots__ = ("_control_points", "_offset")
|
||||
|
||||
def __init__(self, defpoints: Sequence[T]):
|
||||
if len(defpoints) != 4:
|
||||
raise ValueError("Four control points required.")
|
||||
point_type = defpoints[0].__class__
|
||||
if not point_type.__name__ in ("Vec2", "Vec3"): # Cython types!!!
|
||||
raise TypeError(f"invalid point type: {point_type.__name__}")
|
||||
|
||||
# The start point is the curve offset
|
||||
offset: T = defpoints[0]
|
||||
self._offset: T = offset
|
||||
# moving the curve to the origin reduces floating point errors:
|
||||
self._control_points: tuple[T, ...] = tuple(p - offset for p in defpoints)
|
||||
|
||||
@property
|
||||
def control_points(self) -> Sequence[T]:
|
||||
"""Control points as tuple of :class:`Vec3` or :class:`Vec2` objects."""
|
||||
# ezdxf optimization: p0 is always (0, 0, 0)
|
||||
p0, p1, p2, p3 = self._control_points
|
||||
offset = self._offset
|
||||
return offset, p1 + offset, p2 + offset, p3 + offset
|
||||
|
||||
def tangent(self, t: float) -> T:
|
||||
"""Returns direction vector of tangent for location `t` at the
|
||||
Bèzier-curve.
|
||||
|
||||
Args:
|
||||
t: curve position in the range ``[0, 1]``
|
||||
|
||||
"""
|
||||
if not (0 <= t <= 1.0):
|
||||
raise ValueError("t not in range [0 to 1]")
|
||||
return self._get_curve_tangent(t)
|
||||
|
||||
def point(self, t: float) -> T:
|
||||
"""Returns point for location `t` at the Bèzier-curve.
|
||||
|
||||
Args:
|
||||
t: curve position in the range ``[0, 1]``
|
||||
|
||||
"""
|
||||
if not (0 <= t <= 1.0):
|
||||
raise ValueError("t not in range [0 to 1]")
|
||||
return self._get_curve_point(t)
|
||||
|
||||
def approximate(self, segments: int) -> Iterator[T]:
|
||||
"""Approximate `Bézier curve`_ by vertices, yields `segments` + 1
|
||||
vertices as ``(x, y[, z])`` tuples.
|
||||
|
||||
Args:
|
||||
segments: count of segments for approximation
|
||||
|
||||
"""
|
||||
if segments < 1:
|
||||
raise ValueError(segments)
|
||||
delta_t = 1.0 / segments
|
||||
cp = self.control_points
|
||||
yield cp[0]
|
||||
for segment in range(1, segments):
|
||||
yield self._get_curve_point(delta_t * segment)
|
||||
yield cp[3]
|
||||
|
||||
def flattening(self, distance: float, segments: int = 4) -> Iterator[T]:
|
||||
"""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.
|
||||
|
||||
Args:
|
||||
distance: maximum distance from the center of the cubic (C3)
|
||||
curve to the center of the linear (C1) curve between two
|
||||
approximation points to determine if a segment should be
|
||||
subdivided.
|
||||
segments: minimum segment count
|
||||
|
||||
"""
|
||||
stack: list[tuple[float, T]] = []
|
||||
dt: float = 1.0 / segments
|
||||
t0: float = 0.0
|
||||
t1: float
|
||||
cp = self.control_points
|
||||
start_point: T = cp[0]
|
||||
end_point: T
|
||||
|
||||
yield start_point
|
||||
while t0 < 1.0:
|
||||
t1 = t0 + dt
|
||||
if math.isclose(t1, 1.0):
|
||||
end_point = cp[3]
|
||||
t1 = 1.0
|
||||
else:
|
||||
end_point = self._get_curve_point(t1)
|
||||
|
||||
while True:
|
||||
mid_t: float = (t0 + t1) * 0.5
|
||||
mid_point: T = self._get_curve_point(mid_t)
|
||||
chk_point: T = start_point.lerp(end_point)
|
||||
|
||||
d = chk_point.distance(mid_point)
|
||||
if d < distance:
|
||||
yield end_point
|
||||
t0 = t1
|
||||
start_point = end_point
|
||||
if stack:
|
||||
t1, end_point = stack.pop()
|
||||
else:
|
||||
break
|
||||
else:
|
||||
stack.append((t1, end_point))
|
||||
t1 = mid_t
|
||||
end_point = mid_point
|
||||
|
||||
def _get_curve_point(self, t: float) -> T:
|
||||
# 1st control point (p0) is always (0, 0, 0)
|
||||
# => p0 * a is always (0, 0, 0)
|
||||
# add offset at last - it is maybe very large
|
||||
_, p1, p2, p3 = self._control_points
|
||||
t2 = t * t
|
||||
_1_minus_t = 1.0 - t
|
||||
# a = _1_minus_t_square * _1_minus_t
|
||||
b = 3.0 * _1_minus_t * _1_minus_t * t
|
||||
c = 3.0 * _1_minus_t * t2
|
||||
d = t2 * t
|
||||
return p1 * b + p2 * c + p3 * d + self._offset
|
||||
|
||||
def _get_curve_tangent(self, t: float) -> T:
|
||||
# tangent vector is independent from offset location!
|
||||
# 1st control point (p0) is always (0, 0, 0)
|
||||
# => p0 * a is always (0, 0, 0)
|
||||
_, p1, p2, p3 = self._control_points
|
||||
t2 = t * t
|
||||
# a = -3.0 * (1.0 - t) ** 2
|
||||
b = 3.0 * (1.0 - 4.0 * t + 3.0 * t2)
|
||||
c = 3.0 * t * (2.0 - 3.0 * t)
|
||||
d = 3.0 * t2
|
||||
return p1 * b + p2 * c + p3 * d
|
||||
|
||||
def approximated_length(self, segments: int = 128) -> float:
|
||||
"""Returns estimated length of Bèzier-curve as approximation by line
|
||||
`segments`.
|
||||
"""
|
||||
length = 0.0
|
||||
prev_point = None
|
||||
for point in self.approximate(segments):
|
||||
if prev_point is not None:
|
||||
length += prev_point.distance(point)
|
||||
prev_point = point
|
||||
return length
|
||||
|
||||
def reverse(self) -> Bezier4P[T]:
|
||||
"""Returns a new Bèzier-curve with reversed control point order."""
|
||||
return Bezier4P(list(reversed(self.control_points)))
|
||||
|
||||
def transform(self, m: Matrix44) -> Bezier4P[Vec3]:
|
||||
"""General transformation interface, returns a new :class:`Bezier4p`
|
||||
curve as a 3D curve.
|
||||
|
||||
Args:
|
||||
m: 4x4 transformation :class:`Matrix44`
|
||||
|
||||
"""
|
||||
defpoints = Vec3.generate(self.control_points)
|
||||
return Bezier4P(tuple(m.transform_vertices(defpoints)))
|
||||
|
||||
|
||||
def cubic_bezier_from_arc(
|
||||
center: UVec = (0, 0, 0),
|
||||
radius: float = 1,
|
||||
start_angle: float = 0,
|
||||
end_angle: float = 360,
|
||||
segments: int = 1,
|
||||
) -> Iterator[Bezier4P[Vec3]]:
|
||||
"""Returns an approximation for a circular 2D arc by multiple cubic
|
||||
Bézier-curves.
|
||||
|
||||
Args:
|
||||
center: circle center as :class:`Vec3` compatible object
|
||||
radius: circle radius
|
||||
start_angle: start angle in degrees
|
||||
end_angle: end angle in degrees
|
||||
segments: count of Bèzier-curve segments, at least one segment for each
|
||||
quarter (90 deg), 1 for as few as possible.
|
||||
|
||||
"""
|
||||
center_: Vec3 = Vec3(center)
|
||||
radius = float(radius)
|
||||
angle_span: float = arc_angle_span_deg(start_angle, end_angle)
|
||||
if abs(angle_span) < 1e-9:
|
||||
return
|
||||
|
||||
s: float = start_angle
|
||||
start_angle = math.radians(s) % math.tau
|
||||
end_angle = math.radians(s + angle_span)
|
||||
while start_angle > end_angle:
|
||||
end_angle += math.tau
|
||||
|
||||
for control_points in cubic_bezier_arc_parameters(start_angle, end_angle, segments):
|
||||
defpoints = [center_ + (p * radius) for p in control_points]
|
||||
yield Bezier4P(defpoints)
|
||||
|
||||
|
||||
PI_2: float = math.pi / 2.0
|
||||
|
||||
|
||||
def cubic_bezier_from_ellipse(
|
||||
ellipse: "ConstructionEllipse", segments: int = 1
|
||||
) -> Iterator[Bezier4P[Vec3]]:
|
||||
"""Returns an approximation for an elliptic arc by multiple cubic
|
||||
Bézier-curves.
|
||||
|
||||
Args:
|
||||
ellipse: ellipse parameters as :class:`~ezdxf.math.ConstructionEllipse`
|
||||
object
|
||||
segments: count of Bèzier-curve segments, at least one segment for each
|
||||
quarter (π/2), 1 for as few as possible.
|
||||
|
||||
"""
|
||||
param_span: float = ellipse.param_span
|
||||
if abs(param_span) < 1e-9:
|
||||
return
|
||||
start_angle: float = ellipse.start_param % math.tau
|
||||
end_angle: float = start_angle + param_span
|
||||
while start_angle > end_angle:
|
||||
end_angle += math.tau
|
||||
|
||||
def transform(points: Iterable[Vec3]) -> Iterator[Vec3]:
|
||||
center = Vec3(ellipse.center)
|
||||
x_axis: Vec3 = ellipse.major_axis
|
||||
y_axis: Vec3 = ellipse.minor_axis
|
||||
for p in points:
|
||||
yield center + x_axis * p.x + y_axis * p.y
|
||||
|
||||
for defpoints in cubic_bezier_arc_parameters(start_angle, end_angle, segments):
|
||||
yield Bezier4P(tuple(transform(defpoints)))
|
||||
|
||||
|
||||
# Circular arc to Bezier curve:
|
||||
# Source: https://stackoverflow.com/questions/1734745/how-to-create-circle-with-b%C3%A9zier-curves
|
||||
# Optimization: https://spencermortensen.com/articles/bezier-circle/
|
||||
# actual c = 0.5522847498307935 = 4.0/3.0*(sqrt(2)-1.0) and max. deviation of ~0.03%
|
||||
DEFAULT_TANGENT_FACTOR = 4.0 / 3.0 # 1.333333333333333333
|
||||
# optimal c = 0.551915024494 and max. deviation of ~0.02%
|
||||
OPTIMIZED_TANGENT_FACTOR = 1.3324407374108935
|
||||
# Not sure if this is the correct way to apply this optimization,
|
||||
# so i stick to the original version for now:
|
||||
TANGENT_FACTOR = DEFAULT_TANGENT_FACTOR
|
||||
|
||||
|
||||
def cubic_bezier_arc_parameters(
|
||||
start_angle: float, end_angle: float, segments: int = 1
|
||||
) -> Iterator[tuple[Vec3, Vec3, Vec3, Vec3]]:
|
||||
"""Yields cubic Bézier-curve parameters for a circular 2D arc with center
|
||||
at (0, 0) and a radius of 1 in the form of [start point, 1. control point,
|
||||
2. control point, end point].
|
||||
|
||||
Args:
|
||||
start_angle: start angle in radians
|
||||
end_angle: end angle in radians (end_angle > start_angle!)
|
||||
segments: count of Bèzier-curve segments, at least one segment for each
|
||||
quarter (π/2)
|
||||
|
||||
"""
|
||||
if segments < 1:
|
||||
raise ValueError("Invalid argument segments (>= 1).")
|
||||
delta_angle: float = end_angle - start_angle
|
||||
if delta_angle > 0:
|
||||
arc_count = max(math.ceil(delta_angle / math.pi * 2.0), segments)
|
||||
else:
|
||||
raise ValueError("Delta angle from start- to end angle has to be > 0.")
|
||||
|
||||
segment_angle: float = delta_angle / arc_count
|
||||
tangent_length: float = TANGENT_FACTOR * math.tan(segment_angle / 4.0)
|
||||
|
||||
angle: float = start_angle
|
||||
end_point: Vec3 = Vec3.from_angle(angle)
|
||||
for _ in range(arc_count):
|
||||
start_point = end_point
|
||||
angle += segment_angle
|
||||
end_point = Vec3.from_angle(angle)
|
||||
control_point_1 = start_point + (
|
||||
-start_point.y * tangent_length,
|
||||
start_point.x * tangent_length,
|
||||
)
|
||||
control_point_2 = end_point + (
|
||||
end_point.y * tangent_length,
|
||||
-end_point.x * tangent_length,
|
||||
)
|
||||
yield start_point, control_point_1, control_point_2, end_point
|
||||
Reference in New Issue
Block a user