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,39 @@
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
import sys
from ezdxf._options import options
# Set environment variable EZDXF_DISABLE_C_EXT to '1' or 'True' to disable
# the usage of C extensions implemented by Cython.
#
# Important: If you change the EZDXF_DISABLE_C_EXT state, you have to restart
# the Python interpreter, because C extension integration is done at the
# ezdxf import!
#
# Config files:
# Section: core
# Key: disable_c_ext = 1
#
# Direct imports from the C extension modules can not be disabled,
# just the usage by the ezdxf core package.
# For an example see ezdxf.math.__init__, if you import Vec3 from ezdxf.math
# the implementation depends on DISABLE_C_EXT and the existence of the C
# extension, but if you import Vec3 from ezdxf.math.vectors, you always get
# the Python implementation.
USE_C_EXT = not options.disable_c_ext
# C-extensions are always disabled for pypy because JIT compiled Python code is
# much faster!
PYPY = hasattr(sys, 'pypy_version_info')
if PYPY:
USE_C_EXT = False
if USE_C_EXT:
try:
from ezdxf.acc import vector
except ImportError:
USE_C_EXT = False
# set actual state of C-extension usage
options._use_c_ext = USE_C_EXT

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,222 @@
# cython: language_level=3
# Copyright (c) 2021-2024 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence
from .vector cimport Vec3, isclose, v3_dist, v3_lerp, v3_add, Vec2
from .matrix44 cimport Matrix44
import warnings
if TYPE_CHECKING:
from ezdxf.math import UVec
__all__ = ['Bezier3P']
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
cdef double RECURSION_LIMIT = 1000
cdef class Bezier3P:
cdef:
FastQuadCurve curve # pyright: ignore
readonly Vec3 start_point
Vec3 cp1
readonly Vec3 end_point
def __cinit__(self, defpoints: Sequence[UVec]):
if not isinstance(defpoints[0], (Vec2, Vec3)):
warnings.warn(
DeprecationWarning,
"Bezier3P requires defpoints of type Vec2 or Vec3 in the future",
)
if len(defpoints) == 3:
self.start_point = Vec3(defpoints[0])
self.cp1 = Vec3(defpoints[1])
self.end_point = Vec3(defpoints[2])
self.curve = FastQuadCurve(
self.start_point,
self.cp1,
self.end_point
)
else:
raise ValueError("Three control points required.")
@property
def control_points(self) -> tuple[Vec3, Vec3, Vec3]:
return self.start_point, self.cp1, self.end_point
def __reduce__(self):
return Bezier3P, (self.control_points,)
def point(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.point(t)
else:
raise ValueError("t not in range [0 to 1]")
def tangent(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.tangent(t)
else:
raise ValueError("t not in range [0 to 1]")
def approximate(self, int segments) -> list[Vec3]:
cdef double delta_t
cdef int segment
cdef list points = [self.start_point]
if segments < 1:
raise ValueError(segments)
delta_t = 1.0 / segments
for segment in range(1, segments):
points.append(self.point(delta_t * segment))
points.append(self.end_point)
return points
def flattening(self, double distance, int segments = 4) -> list[Vec3]:
cdef double dt = 1.0 / segments
cdef double t0 = 0.0, t1
cdef _Flattening f = _Flattening(self, distance)
cdef Vec3 start_point = self.start_point
cdef Vec3 end_point
while t0 < 1.0:
t1 = t0 + dt
if isclose(t1, 1.0, REL_TOL, ABS_TOL):
end_point = self.end_point
t1 = 1.0
else:
end_point = self.curve.point(t1)
f.reset_recursion_check()
f.flatten(start_point, end_point, t0, t1)
if f.has_recursion_error():
raise RecursionError(
"Bezier3P flattening error, check for very large coordinates"
)
t0 = t1
start_point = end_point
return f.points
def approximated_length(self, segments: int = 128) -> float:
cdef double length = 0.0
cdef bint start_flag = 0
cdef Vec3 prev_point, point
for point in self.approximate(segments):
if start_flag:
length += v3_dist(prev_point, point)
else:
start_flag = 1
prev_point = point
return length
def reverse(self) -> Bezier3P:
return Bezier3P((self.end_point, self.cp1, self.start_point))
def transform(self, Matrix44 m) -> Bezier3P:
return Bezier3P(tuple(m.transform_vertices(self.control_points)))
cdef class _Flattening:
cdef FastQuadCurve curve # pyright: ignore
cdef double distance
cdef list points
cdef int _recursion_level
cdef int _recursion_error
def __cinit__(self, Bezier3P curve, double distance):
self.curve = curve.curve
self.distance = distance
self.points = [curve.start_point]
self._recursion_level = 0
self._recursion_error = 0
cdef has_recursion_error(self):
return self._recursion_error
cdef reset_recursion_check(self):
self._recursion_level = 0
self._recursion_error = 0
cdef flatten(
self,
Vec3 start_point,
Vec3 end_point,
double start_t,
double end_t
):
if self._recursion_level > RECURSION_LIMIT:
self._recursion_error = 1
return
self._recursion_level += 1
cdef double mid_t = (start_t + end_t) * 0.5
cdef Vec3 mid_point = self.curve.point(mid_t)
cdef double d = v3_dist(mid_point, v3_lerp(start_point, end_point, 0.5))
if d < self.distance:
self.points.append(end_point)
else:
self.flatten(start_point, mid_point, start_t, mid_t)
self.flatten(mid_point, end_point, mid_t, end_t)
self._recursion_level -= 1
cdef class FastQuadCurve:
cdef:
double[3] offset
double[3] p1
double[3] p2
def __cinit__(self, Vec3 p0, Vec3 p1, Vec3 p2):
self.offset[0] = p0.x
self.offset[1] = p0.y
self.offset[2] = p0.z
# 1st control point (p0) is always (0, 0, 0)
self.p1[0] = p1.x - p0.x
self.p1[1] = p1.y - p0.y
self.p1[2] = p1.z - p0.z
self.p2[0] = p2.x - p0.x
self.p2[1] = p2.y - p0.y
self.p2[2] = p2.z - p0.z
cdef Vec3 point(self, double t):
# 1st control point (p0) is always (0, 0, 0)
# => p0 * a is always (0, 0, 0)
cdef:
Vec3 result = Vec3()
# double a = (1 - t) ** 2
double b = 2.0 * t * (1.0 - t)
double c = t * t
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
# add offset at last - it is maybe very large
result.x += self.offset[0]
result.y += self.offset[1]
result.z += self.offset[2]
return result
cdef Vec3 tangent(self, double t):
# tangent vector is independent from offset location!
cdef:
Vec3 result = Vec3()
# double a = -2 * (1 - t)
double b = 2.0 - 4.0 * t
double c = 2.0 * t
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
return result
cdef void iadd_mul(Vec3 a, double[3] b, double c):
a.x += b[0] * c
a.y += b[1] * c
a.z += b[2] * c

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,371 @@
# cython: language_level=3
# Copyright (c) 2020-2024 Manfred Moitzi
# License: MIT License
from typing import TYPE_CHECKING, Sequence, Iterable, Iterator
import cython
import warnings
from .vector cimport (
Vec3,
Vec2,
isclose,
v3_add,
v3_mul,
v3_dist,
v3_lerp,
v3_from_angle,
normalize_rad_angle,
)
from .matrix44 cimport Matrix44
from libc.math cimport ceil, tan, M_PI
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',
]
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const double M_TAU
cdef double DEG2RAD = M_PI / 180.0
cdef double RECURSION_LIMIT = 1000
cdef class Bezier4P:
cdef:
FastCubicCurve curve # pyright: ignore
readonly Vec3 start_point
Vec3 cp1
Vec3 cp2
readonly Vec3 end_point
def __cinit__(self, defpoints: Sequence[UVec]):
if not isinstance(defpoints[0], (Vec2, Vec3)):
warnings.warn(
DeprecationWarning,
"Bezier4P requires defpoints of type Vec2 or Vec3 in the future",
)
if len(defpoints) == 4:
self.start_point = Vec3(defpoints[0])
self.cp1 = Vec3(defpoints[1])
self.cp2 = Vec3(defpoints[2])
self.end_point = Vec3(defpoints[3])
self.curve = FastCubicCurve(
self.start_point,
self.cp1,
self.cp2,
self.end_point
)
else:
raise ValueError("Four control points required.")
@property
def control_points(self) -> tuple[Vec3, Vec3, Vec3, Vec3]:
return self.start_point, self.cp1, self.cp2, self.end_point
def __reduce__(self):
return Bezier4P, (self.control_points,)
def point(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.point(t)
else:
raise ValueError("t not in range [0 to 1]")
def tangent(self, double t) -> Vec3:
if 0.0 <= t <= 1.0:
return self.curve.tangent(t)
else:
raise ValueError("t not in range [0 to 1]")
def approximate(self, int segments) -> list[Vec3]:
cdef double delta_t
cdef int segment
cdef list points = [self.start_point]
if segments < 1:
raise ValueError(segments)
delta_t = 1.0 / segments
for segment in range(1, segments):
points.append(self.curve.point(delta_t * segment))
points.append(self.end_point)
return points
def flattening(self, double distance, int segments = 4) -> list[Vec3]:
cdef double dt = 1.0 / segments
cdef double t0 = 0.0, t1
cdef _Flattening f = _Flattening(self, distance)
cdef Vec3 start_point = self.start_point
cdef Vec3 end_point
while t0 < 1.0:
t1 = t0 + dt
if isclose(t1, 1.0, REL_TOL, ABS_TOL):
end_point = self.end_point
t1 = 1.0
else:
end_point = self.curve.point(t1)
f.reset_recursion_check()
f.flatten(start_point, end_point, t0, t1)
if f.has_recursion_error():
raise RecursionError(
"Bezier4P flattening error, check for very large coordinates"
)
t0 = t1
start_point = end_point
return f.points
def approximated_length(self, segments: int = 128) -> float:
cdef double length = 0.0
cdef bint start_flag = 0
cdef Vec3 prev_point, point
for point in self.approximate(segments):
if start_flag:
length += v3_dist(prev_point, point)
else:
start_flag = 1
prev_point = point
return length
def reverse(self) -> Bezier4P:
return Bezier4P((self.end_point, self.cp2, self.cp1, self.start_point))
def transform(self, Matrix44 m) -> Bezier4P:
return Bezier4P(tuple(m.transform_vertices(self.control_points)))
cdef class _Flattening:
cdef FastCubicCurve curve # pyright: ignore
cdef double distance
cdef list points
cdef int _recursion_level
cdef int _recursion_error
def __cinit__(self, Bezier4P curve, double distance):
self.curve = curve.curve
self.distance = distance
self.points = [curve.start_point]
self._recursion_level = 0
self._recursion_error = 0
cdef has_recursion_error(self):
return self._recursion_error
cdef reset_recursion_check(self):
self._recursion_level = 0
self._recursion_error = 0
cdef flatten(
self,
Vec3 start_point,
Vec3 end_point,
double start_t,
double end_t
):
# Keep in sync with CPython implementation: ezdxf/math/_bezier4p.py
# Test suite: 630a
if self._recursion_level > RECURSION_LIMIT:
self._recursion_error = 1
return
self._recursion_level += 1
cdef double mid_t = (start_t + end_t) * 0.5
cdef Vec3 mid_point = self.curve.point(mid_t)
cdef double d = v3_dist(mid_point, v3_lerp(start_point, end_point, 0.5))
if d < self.distance:
self.points.append(end_point)
else:
self.flatten(start_point, mid_point, start_t, mid_t)
self.flatten(mid_point, end_point, mid_t, end_t)
self._recursion_level -= 1
cdef double DEFAULT_TANGENT_FACTOR = 4.0 / 3.0 # 1.333333333333333333
cdef double OPTIMIZED_TANGENT_FACTOR = 1.3324407374108935
cdef double TANGENT_FACTOR = DEFAULT_TANGENT_FACTOR
@cython.cdivision(True)
def cubic_bezier_arc_parameters(
double start_angle,
double end_angle,
int segments = 1,
) -> Iterator[tuple[Vec3, Vec3, Vec3, Vec3]]:
if segments < 1:
raise ValueError('Invalid argument segments (>= 1).')
cdef double delta_angle = end_angle - start_angle
cdef int arc_count
if delta_angle > 0:
arc_count = <int> ceil(delta_angle / M_PI * 2.0)
if segments > arc_count:
arc_count = segments
else:
raise ValueError('Delta angle from start- to end angle has to be > 0.')
cdef double segment_angle = delta_angle / arc_count
cdef double tangent_length = TANGENT_FACTOR * tan(segment_angle / 4.0)
cdef double angle = start_angle
cdef Vec3 start_point, end_point, cp1, cp2
end_point = v3_from_angle(angle, 1.0)
for _ in range(arc_count):
start_point = end_point
angle += segment_angle
end_point = v3_from_angle(angle, 1.0)
cp1 = Vec3()
cp1.x = start_point.x - start_point.y * tangent_length
cp1.y = start_point.y + start_point.x * tangent_length
cp2 = Vec3()
cp2.x = end_point.x + end_point.y * tangent_length
cp2.y = end_point.y - end_point.x * tangent_length
yield start_point, cp1, cp2, end_point
def cubic_bezier_from_arc(
center = (0, 0),
double radius = 1.0,
double start_angle = 0.0,
double end_angle = 360.0,
int segments = 1
) -> Iterable[Bezier4P]:
cdef Vec3 center_ = Vec3(center)
cdef Vec3 tmp
cdef list res
cdef int i
cdef double angle_span = arc_angle_span_deg(start_angle, end_angle)
if abs(angle_span) < 1e-9:
return
cdef double s = start_angle
start_angle = (s * DEG2RAD) % M_TAU
end_angle = (s + angle_span) * DEG2RAD
while start_angle > end_angle:
end_angle += M_TAU
for control_points in cubic_bezier_arc_parameters(start_angle, end_angle, segments):
res = []
for i in range(4):
tmp = <Vec3> control_points[i]
res.append(v3_add(center_, v3_mul(tmp, radius)))
yield Bezier4P(res)
def cubic_bezier_from_ellipse(
ellipse: ConstructionEllipse,
int segments = 1
) -> Iterator[Bezier4P]:
cdef double param_span = ellipse.param_span
if abs(param_span) < 1e-9:
return
cdef double start_angle = normalize_rad_angle(ellipse.start_param)
cdef double end_angle = start_angle + param_span
while start_angle > end_angle:
end_angle += M_TAU
cdef Vec3 center = Vec3(ellipse.center)
cdef Vec3 x_axis = Vec3(ellipse.major_axis)
cdef Vec3 y_axis = Vec3(ellipse.minor_axis)
cdef Vec3 cp
cdef Vec3 c_res
cdef list res
for control_points in cubic_bezier_arc_parameters(start_angle, end_angle, segments):
res = list()
for i in range(4):
cp = <Vec3> control_points[i]
c_res = v3_add_3(center, v3_mul(x_axis, cp.x), v3_mul(y_axis, cp.y))
res.append(c_res)
yield Bezier4P(res)
cdef Vec3 v3_add_3(Vec3 a, Vec3 b, Vec3 c):
cdef Vec3 result = Vec3()
result.x = a.x + b.x + c.x
result.y = a.y + b.y + c.y
result.z = a.z + b.z + c.z
return result
cdef class FastCubicCurve:
cdef:
double[3] offset
double[3] p1
double[3] p2
double[3] p3
def __cinit__(self, Vec3 p0, Vec3 p1, Vec3 p2, Vec3 p3):
cdef:
double x = p0.x
double y = p0.y
double z = p0.z
self.offset[0] = x
self.offset[1] = y
self.offset[2] = z
# 1st control point (p0) is always (0, 0, 0)
self.p1[0] = p1.x - x
self.p1[1] = p1.y - y
self.p1[2] = p1.z - z
self.p2[0] = p2.x - x
self.p2[1] = p2.y - y
self.p2[2] = p2.z - z
self.p3[0] = p3.x - x
self.p3[1] = p3.y - y
self.p3[2] = p3.z - z
cdef Vec3 point(self, double t):
# 1st control point (p0) is always (0, 0, 0)
# => p0 * a is always (0, 0, 0)
cdef:
Vec3 result = Vec3()
double t2 = t * t
double _1_minus_t = 1.0 - t
# a = (1 - t) ** 3
double b = 3.0 * _1_minus_t * _1_minus_t * t
double c = 3.0 * _1_minus_t * t2
double d = t2 * t
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
iadd_mul(result, self.p3, d)
# add offset at last - it is maybe very large
result.x += self.offset[0]
result.y += self.offset[1]
result.z += self.offset[2]
return result
cdef Vec3 tangent(self, double t):
# tangent vector is independent from offset location!
cdef:
Vec3 result = Vec3()
double t2 = t * t
# a = -3 * (1 - t) ** 2
double b = 3.0 * (1.0 - 4.0 * t + 3.0 * t2)
double c = 3.0 * t * (2.0 - 3.0 * t)
double d = 3.0 * t2
iadd_mul(result, self.p1, b)
iadd_mul(result, self.p2, c)
iadd_mul(result, self.p3, d)
return result
cdef void iadd_mul(Vec3 a, double[3] b, double c):
a.x += b[0] * c
a.y += b[1] * c
a.z += b[2] * c

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,400 @@
# cython: language_level=3
# Copyright (c) 2021-2024, Manfred Moitzi
# License: MIT License
from typing import Iterable, Sequence, Iterator
import cython
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from .vector cimport Vec3, isclose, v3_mul, v3_sub
__all__ = ['Basis', 'Evaluator']
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const int MAX_SPLINE_ORDER
# factorial from 0 to 18
cdef double[19] FACTORIAL = [
1., 1., 2., 6., 24., 120., 720., 5040., 40320., 362880., 3628800.,
39916800., 479001600., 6227020800., 87178291200., 1307674368000.,
20922789888000., 355687428096000., 6402373705728000.
]
NULL_LIST = [0.0]
ONE_LIST = [1.0]
cdef Vec3 NULLVEC = Vec3()
@cython.cdivision(True)
cdef double binomial_coefficient(int k, int i):
cdef double k_fact = FACTORIAL[k]
cdef double i_fact = FACTORIAL[i]
cdef double k_i_fact
if i > k:
return 0.0
k_i_fact = FACTORIAL[k - i]
return k_fact / (k_i_fact * i_fact)
@cython.boundscheck(False)
cdef int bisect_right(double *a, double x, int lo, int hi):
cdef int mid
while lo < hi:
mid = (lo + hi) // 2
if x < a[mid]:
hi = mid
else:
lo = mid + 1
return lo
cdef reset_double_array(double *a, int count, double value):
cdef int i
for i in range(count):
a[i] = value
cdef class Basis:
""" Immutable Basis function class. """
# public:
cdef readonly int order
cdef readonly int count
cdef readonly double max_t
cdef tuple weights_ # public attribute for Cython Evaluator
# private:
cdef double *_knots
cdef int knot_count
def __cinit__(
self, knots: Iterable[float],
int order,
int count,
weights: Sequence[float] = None
):
if order < 2 or order >= MAX_SPLINE_ORDER:
raise ValueError('invalid order')
self.order = order
if count < 2:
raise ValueError('invalid count')
self.count = count
self.knot_count = self.order + self.count
self.weights_ = tuple(float(x) for x in weights) if weights else tuple()
cdef Py_ssize_t i = len(self.weights_)
if i != 0 and i != self.count:
raise ValueError('invalid weight count')
knots = [float(x) for x in knots]
if len(knots) != self.knot_count:
raise ValueError('invalid knot count')
self._knots = <double *> PyMem_Malloc(self.knot_count * sizeof(double))
for i in range(self.knot_count):
self._knots[i] = knots[i]
self.max_t = self._knots[self.knot_count - 1]
def __dealloc__(self):
PyMem_Free(self._knots)
@property
def degree(self) -> int:
return self.order - 1
@property
def knots(self) -> tuple[float, ...]:
return tuple(x for x in self._knots[:self.knot_count])
@property
def weights(self) -> tuple[float, ...]:
return self.weights_
@property
def is_rational(self) -> bool:
""" Returns ``True`` if curve is a rational B-spline. (has weights) """
return bool(self.weights_)
cpdef list basis_vector(self, double t):
""" Returns the expanded basis vector. """
cdef int span = self.find_span(t)
cdef int p = self.order - 1
cdef int front = span - p
cdef int back = self.count - span - 1
cdef list result
if front > 0:
result = NULL_LIST * front
result.extend(self.basis_funcs(span, t))
else:
result = self.basis_funcs(span, t)
if back > 0:
result.extend(NULL_LIST * back)
return result
cpdef int find_span(self, double u):
""" Determine the knot span index. """
# Linear search is more reliable than binary search of the Algorithm A2.1
# from The NURBS Book by Piegl & Tiller.
cdef double *knots = self._knots
cdef int count = self.count # text book: n+1
cdef int p = self.order - 1
cdef int span
if u >= knots[count]: # special case
return count - 1
# common clamped spline:
if knots[p] == 0.0: # use binary search
# This is fast and works most of the time,
# but Test 621 : test_weired_closed_spline()
# goes into an infinity loop, because of
# a weird knot configuration.
return bisect_right(knots, u, p, count) - 1
else: # use linear search
span = 0
while knots[span] <= u and span < count:
span += 1
return span - 1
cpdef list basis_funcs(self, int span, double u):
# Source: The NURBS Book: Algorithm A2.2
cdef int order = self.order
cdef double *knots = self._knots
cdef double[MAX_SPLINE_ORDER] N, left, right
cdef list result
reset_double_array(N, order, 0.0)
reset_double_array(left, order, 0.0)
reset_double_array(right, order, 0.0)
cdef int j, r, i1
cdef double temp, saved, temp_r, temp_l
N[0] = 1.0
for j in range(1, order):
i1 = span + 1 - j
if i1 < 0:
i1 = 0
left[j] = u - knots[i1]
right[j] = knots[span + j] - u
saved = 0.0
for r in range(j):
temp_r = right[r + 1]
temp_l = left[j - r]
temp = N[r] / (temp_r + temp_l)
N[r] = saved + temp_r * temp
saved = temp_l * temp
N[j] = saved
result = [x for x in N[:order]]
if self.is_rational:
return self.span_weighting(result, span)
else:
return result
cpdef list span_weighting(self, nbasis: list[float], int span):
cdef list products = [
nb * w for nb, w in zip(
nbasis,
self.weights_[span - self.order + 1: span + 1]
)
]
s = sum(products)
if s != 0:
return [p / s for p in products]
else:
return NULL_LIST * len(nbasis)
cpdef list basis_funcs_derivatives(self, int span, double u, int n = 1):
# pyright: reportUndefinedVariable=false
# pyright flags Cython multi-arrays incorrect:
# cdef double[4][4] a # this is a valid array definition in Cython!
# https://cython.readthedocs.io/en/latest/src/userguide/language_basics.html#c-arrays
# Source: The NURBS Book: Algorithm A2.3
cdef int order = self.order
cdef int p = order - 1
if n > p:
n = p
cdef double *knots = self._knots
cdef double[MAX_SPLINE_ORDER] left, right
reset_double_array(left, order, 1.0)
reset_double_array(right, order, 1.0)
cdef double[MAX_SPLINE_ORDER][MAX_SPLINE_ORDER] ndu # pyright: ignore
reset_double_array(<double *> ndu, MAX_SPLINE_ORDER*MAX_SPLINE_ORDER, 1.0)
cdef int j, r, i1
cdef double temp, saved, tmp_r, tmp_l
for j in range(1, order):
i1 = span + 1 - j
if i1 < 0:
i1 = 0
left[j] = u - knots[i1]
right[j] = knots[span + j] - u
saved = 0.0
for r in range(j):
# lower triangle
tmp_r = right[r + 1]
tmp_l = left[j - r]
ndu[j][r] = tmp_r + tmp_l
temp = ndu[r][j - 1] / ndu[j][r]
# upper triangle
ndu[r][j] = saved + (tmp_r * temp)
saved = tmp_l * temp
ndu[j][j] = saved
# load the basis_vector functions
cdef double[MAX_SPLINE_ORDER][MAX_SPLINE_ORDER] derivatives # pyright: ignore
reset_double_array(
<double *> derivatives, MAX_SPLINE_ORDER*MAX_SPLINE_ORDER, 0.0
)
for j in range(order):
derivatives[0][j] = ndu[j][p]
# loop over function index
cdef double[2][MAX_SPLINE_ORDER] a # pyright: ignore
reset_double_array(<double *> a, 2*MAX_SPLINE_ORDER, 1.0)
cdef int s1, s2, k, rk, pk, j1, j2, t
cdef double d
for r in range(order):
s1 = 0
s2 = 1
# alternate rows in array a
a[0][0] = 1.0
# loop to compute kth derivative
for k in range(1, n + 1):
d = 0.0
rk = r - k
pk = p - k
if r >= k:
a[s2][0] = a[s1][0] / ndu[pk + 1][rk]
d = a[s2][0] * ndu[rk][pk]
if rk >= -1:
j1 = 1
else:
j1 = -rk
if (r - 1) <= pk:
j2 = k - 1
else:
j2 = p - r
for j in range(j1, j2 + 1):
a[s2][j] = (a[s1][j] - a[s1][j - 1]) / ndu[pk + 1][rk + j]
d += (a[s2][j] * ndu[rk + j][pk])
if r <= pk:
a[s2][k] = -a[s1][k - 1] / ndu[pk + 1][r]
d += (a[s2][k] * ndu[r][pk])
derivatives[k][r] = d
# Switch rows
t = s1
s1 = s2
s2 = t
# Multiply through by the correct factors
cdef double rr = p
for k in range(1, n + 1):
for j in range(order):
derivatives[k][j] *= rr
rr *= (p - k)
# return result as Python lists
cdef list result = [], row
for k in range(0, n + 1):
row = []
result.append(row)
for j in range(order):
row.append(derivatives[k][j])
return result
cdef class Evaluator:
""" B-spline curve point and curve derivative evaluator. """
cdef Basis _basis
cdef tuple _control_points
def __cinit__(self, basis: Basis, control_points: Sequence[Vec3]):
self._basis = basis
self._control_points = Vec3.tuple(control_points)
cpdef Vec3 point(self, double u):
# Source: The NURBS Book: Algorithm A3.1
cdef Basis basis = self._basis
if isclose(u, basis.max_t, REL_TOL, ABS_TOL):
u = basis.max_t
cdef:
int p = basis.order - 1
int span = basis.find_span(u)
list N = basis.basis_funcs(span, u)
int i
Vec3 cpoint, v3_sum = Vec3()
tuple control_points = self._control_points
double factor
for i in range(p + 1):
factor = <double> N[i]
cpoint = <Vec3> control_points[span - p + i]
v3_sum.x += cpoint.x * factor
v3_sum.y += cpoint.y * factor
v3_sum.z += cpoint.z * factor
return v3_sum
def points(self, t: Iterable[float]) -> Iterator[Vec3]:
cdef double u
for u in t:
yield self.point(u)
cpdef list derivative(self, double u, int n = 1):
""" Return point and derivatives up to n <= degree for parameter u. """
# Source: The NURBS Book: Algorithm A3.2
cdef Basis basis = self._basis
if isclose(u, basis.max_t, REL_TOL, ABS_TOL):
u = basis.max_t
cdef:
list CK = [], CKw = [], wders = []
tuple control_points = self._control_points
tuple weights
Vec3 cpoint, v3_sum
double wder, bas_func_weight, bas_func
int k, j, i, p = basis.degree
int span = basis.find_span(u)
list basis_funcs_ders = basis.basis_funcs_derivatives(span, u, n)
if basis.is_rational:
# Homogeneous point representation required:
# (x*w, y*w, z*w, w)
weights = basis.weights_
for k in range(n + 1):
v3_sum = Vec3()
wder = 0.0
for j in range(p + 1):
i = span - p + j
bas_func_weight = basis_funcs_ders[k][j] * weights[i]
# control_point * weight * bas_func_der = (x*w, y*w, z*w) * bas_func_der
cpoint = <Vec3> control_points[i]
v3_sum.x += cpoint.x * bas_func_weight
v3_sum.y += cpoint.y * bas_func_weight
v3_sum.z += cpoint.z * bas_func_weight
wder += bas_func_weight
CKw.append(v3_sum)
wders.append(wder)
# Source: The NURBS Book: Algorithm A4.2
for k in range(n + 1):
v3_sum = CKw[k]
for j in range(1, k + 1):
bas_func_weight = binomial_coefficient(k, j) * wders[j]
v3_sum = v3_sub(
v3_sum,
v3_mul(CK[k - j], bas_func_weight)
)
CK.append(v3_sum / wders[0])
else:
for k in range(n + 1):
v3_sum = Vec3()
for j in range(p + 1):
bas_func = basis_funcs_ders[k][j]
cpoint = <Vec3> control_points[span - p + j]
v3_sum.x += cpoint.x * bas_func
v3_sum.y += cpoint.y * bas_func
v3_sum.z += cpoint.z * bas_func
CK.append(v3_sum)
return CK
def derivatives(self, t: Iterable[float], int n = 1) -> Iterator[list[Vec3]]:
cdef double u
for u in t:
yield self.derivative(u, n)

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2023, Manfred Moitzi
// License: MIT License
// global constants
#define ABS_TOL 1e-12
#define REL_TOL 1e-9
#define M_TAU 6.283185307179586
// AutoCAD limits the degree of SPLINE to 11 or order = 12
#define MAX_SPLINE_ORDER 12

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,359 @@
# cython: language_level=3
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
from typing import Iterable, TYPE_CHECKING, Sequence, Optional
from libc.math cimport fabs, M_PI, M_PI_2, M_PI_4, M_E, sin, tan, pow, atan, log
from .vector cimport (
isclose,
Vec2,
v2_isclose,
Vec3,
v3_sub,
v3_add,
v3_mul,
v3_normalize,
v3_cross,
v3_magnitude_sqr,
v3_isclose,
)
import cython
if TYPE_CHECKING:
from ezdxf.math import UVec
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const double M_TAU
cdef double RAD_ABS_TOL = 1e-15
cdef double DEG_ABS_TOL = 1e-13
cdef double TOLERANCE = 1e-10
def has_clockwise_orientation(vertices: Iterable[UVec]) -> bool:
""" Returns True if 2D `vertices` have clockwise orientation. Ignores
z-axis of all vertices.
Args:
vertices: iterable of :class:`Vec2` compatible objects
Raises:
ValueError: less than 3 vertices
"""
cdef list _vertices = [Vec2(v) for v in vertices]
if len(_vertices) < 3:
raise ValueError('At least 3 vertices required.')
cdef Vec2 p1 = <Vec2> _vertices[0]
cdef Vec2 p2 = <Vec2> _vertices[-1]
cdef double s = 0.0
cdef Py_ssize_t index
# Using the same tolerance as the Python implementation:
if not v2_isclose(p1, p2, REL_TOL, ABS_TOL):
_vertices.append(p1)
for index in range(1, len(_vertices)):
p2 = <Vec2> _vertices[index]
s += (p2.x - p1.x) * (p2.y + p1.y)
p1 = p2
return s > 0.0
def intersection_line_line_2d(
line1: Sequence[Vec2],
line2: Sequence[Vec2],
bint virtual=True,
double abs_tol=TOLERANCE) -> Optional[Vec2]:
"""
Compute the intersection of two lines in the xy-plane.
Args:
line1: start- and end point of first line to test
e.g. ((x1, y1), (x2, y2)).
line2: start- and end point of second line to test
e.g. ((x3, y3), (x4, y4)).
virtual: ``True`` returns any intersection point, ``False`` returns
only real intersection points.
abs_tol: tolerance for intersection test.
Returns:
``None`` if there is no intersection point (parallel lines) or
intersection point as :class:`Vec2`
"""
# Algorithm based on: http://paulbourke.net/geometry/pointlineplane/
# chapter: Intersection point of two line segments in 2 dimensions
cdef Vec2 s1, s2, c1, c2, res
cdef double s1x, s1y, s2x, s2y, c1x, c1y, c2x, c2y, den, us, uc
cdef double lwr = 0.0, upr = 1.0
s1 = line1[0]
s2 = line1[1]
c1 = line2[0]
c2 = line2[1]
s1x = s1.x
s1y = s1.y
s2x = s2.x
s2y = s2.y
c1x = c1.x
c1y = c1.y
c2x = c2.x
c2y = c2.y
den = (c2y - c1y) * (s2x - s1x) - (c2x - c1x) * (s2y - s1y)
if fabs(den) <= abs_tol:
return None
# den near zero is checked by if-statement above:
with cython.cdivision(True):
us = ((c2x - c1x) * (s1y - c1y) - (c2y - c1y) * (s1x - c1x)) / den
res = Vec2(s1x + us * (s2x - s1x), s1y + us * (s2y - s1y))
if virtual:
return res
# 0 = intersection point is the start point of the line
# 1 = intersection point is the end point of the line
# otherwise: linear interpolation
if lwr <= us <= upr: # intersection point is on the subject line
with cython.cdivision(True):
uc = ((s2x - s1x) * (s1y - c1y) - (s2y - s1y) * (s1x - c1x)) / den
if lwr <= uc <= upr: # intersection point is on the clipping line
return res
return None
cdef double _determinant(Vec3 v1, Vec3 v2, Vec3 v3):
return v1.x * v2.y * v3.z + v1.y * v2.z * v3.x + \
v1.z * v2.x * v3.y - v1.z * v2.y * v3.x - \
v1.x * v2.z * v3.y - v1.y * v2.x * v3.z
def intersection_ray_ray_3d(
ray1: tuple[Vec3, Vec3],
ray2: tuple[Vec3, Vec3],
double abs_tol=TOLERANCE
) -> Sequence[Vec3]:
"""
Calculate intersection of two 3D rays, returns a 0-tuple for parallel rays,
a 1-tuple for intersecting rays and a 2-tuple for not intersecting and not
parallel rays with points of closest approach on each ray.
Args:
ray1: first ray as tuple of two points as Vec3() objects
ray2: second ray as tuple of two points as Vec3() objects
abs_tol: absolute tolerance for comparisons
"""
# source: http://www.realtimerendering.com/intersections.html#I304
cdef:
Vec3 o2_o1
double det1, det2
Vec3 o1 = Vec3(ray1[0])
Vec3 p1 = Vec3(ray1[1])
Vec3 o2 = Vec3(ray2[0])
Vec3 p2 = Vec3(ray2[1])
Vec3 d1 = v3_normalize(v3_sub(p1, o1), 1.0)
Vec3 d2 = v3_normalize(v3_sub(p2, o2), 1.0)
Vec3 d1xd2 = v3_cross(d1, d2)
double denominator = v3_magnitude_sqr(d1xd2)
if denominator <= abs_tol:
# ray1 is parallel to ray2
return tuple()
else:
o2_o1 = v3_sub(o2, o1)
det1 = _determinant(o2_o1, d2, d1xd2)
det2 = _determinant(o2_o1, d1, d1xd2)
with cython.cdivision(True): # denominator check is already done
p1 = v3_add(o1, v3_mul(d1, (det1 / denominator)))
p2 = v3_add(o2, v3_mul(d2, (det2 / denominator)))
if v3_isclose(p1, p2, abs_tol, abs_tol):
# ray1 and ray2 have an intersection point
return p1,
else:
# ray1 and ray2 do not have an intersection point,
# p1 and p2 are the points of closest approach on each ray
return p1, p2
def arc_angle_span_deg(double start, double end) -> float:
if isclose(start, end, REL_TOL, DEG_ABS_TOL):
return 0.0
start %= 360.0
if isclose(start, end % 360.0, REL_TOL, DEG_ABS_TOL):
return 360.0
if not isclose(end, 360.0, REL_TOL, DEG_ABS_TOL):
end %= 360.0
if end < start:
end += 360.0
return end - start
def arc_angle_span_rad(double start, double end) -> float:
if isclose(start, end, REL_TOL, RAD_ABS_TOL):
return 0.0
start %= M_TAU
if isclose(start, end % M_TAU, REL_TOL, RAD_ABS_TOL):
return M_TAU
if not isclose(end, M_TAU, REL_TOL, RAD_ABS_TOL):
end %= M_TAU
if end < start:
end += M_TAU
return end - start
def is_point_in_polygon_2d(
point: Vec2, polygon: list[Vec2], double abs_tol=TOLERANCE
) -> int:
"""
Test if `point` is inside `polygon`. Returns +1 for inside, 0 for on the
boundary and -1 for outside.
Supports convex and concave polygons with clockwise or counter-clockwise oriented
polygon vertices. Does not raise an exception for degenerated polygons.
Args:
point: 2D point to test as :class:`Vec2`
polygon: list of 2D points as :class:`Vec2`
abs_tol: tolerance for distance check
Returns:
+1 for inside, 0 for on the boundary, -1 for outside
"""
# Source: http://www.faqs.org/faqs/graphics/algorithms-faq/
# Subject 2.03: How do I find if a point lies within a polygon?
# Numpy version was just 10x faster, this version is 23x faster than the Python
# version!
cdef double a, b, c, d, x, y, x1, y1, x2, y2
cdef list vertices = polygon
cdef Vec2 p1, p2
cdef int size, last, i
cdef bint inside = 0
size = len(vertices)
if size < 3: # empty polygon
return -1
last = size - 1
p1 = <Vec2> vertices[0]
p2 = <Vec2> vertices[last]
if v2_isclose(p1, p2, REL_TOL, ABS_TOL): # open polygon
size -= 1
last -= 1
if size < 3:
return -1
x = point.x
y = point.y
p1 = <Vec2> vertices[last]
x1 = p1.x
y1 = p1.y
for i in range(size):
p2 = <Vec2> vertices[i]
x2 = p2.x
y2 = p2.y
# is point on polygon boundary line:
# is point in x-range of line
a, b = (x2, x1) if x2 < x1 else (x1, x2)
if a <= x <= b:
# is point in y-range of line
c, d = (y2, y1) if y2 < y1 else (y1, y2)
if (c <= y <= d) and fabs(
(y2 - y1) * x - (x2 - x1) * y + (x2 * y1 - y2 * x1)
) <= abs_tol:
return 0 # on boundary line
if ((y1 <= y < y2) or (y2 <= y < y1)) and (
x < (x2 - x1) * (y - y1) / (y2 - y1) + x1
):
inside = not inside
x1 = x2
y1 = y2
if inside:
return 1 # inside polygon
else:
return -1 # outside polygon
cdef double WGS84_SEMI_MAJOR_AXIS = 6378137
cdef double WGS84_SEMI_MINOR_AXIS = 6356752.3142
cdef double WGS84_ELLIPSOID_ECCENTRIC = 0.08181919092890624
cdef double RADIANS = M_PI / 180.0
cdef double DEGREES = 180.0 / M_PI
def gps_to_world_mercator(double longitude, double latitude) -> tuple[float, float]:
"""Transform GPS (long/lat) to World Mercator.
Transform WGS84 `EPSG:4326 <https://epsg.io/4326>`_ location given as
latitude and longitude in decimal degrees as used by GPS into World Mercator
cartesian 2D coordinates in meters `EPSG:3395 <https://epsg.io/3395>`_.
Args:
longitude: represents the longitude value (East-West) in decimal degrees
latitude: represents the latitude value (North-South) in decimal degrees.
"""
# From: https://epsg.io/4326
# EPSG:4326 WGS84 - World Geodetic System 1984, used in GPS
# To: https://epsg.io/3395
# EPSG:3395 - World Mercator
# Source: https://gis.stackexchange.com/questions/259121/transformation-functions-for-epsg3395-projection-vs-epsg3857
longitude = longitude * RADIANS # east
latitude = latitude * RADIANS # north
cdef double e_sin_lat = sin(latitude) * WGS84_ELLIPSOID_ECCENTRIC
cdef double c = pow(
(1.0 - e_sin_lat) / (1.0 + e_sin_lat), WGS84_ELLIPSOID_ECCENTRIC / 2.0
) # 7-7 p.44
y = WGS84_SEMI_MAJOR_AXIS * log(tan(M_PI_4 + latitude / 2.0) * c) # 7-7 p.44
x = WGS84_SEMI_MAJOR_AXIS * longitude
return x, y
def world_mercator_to_gps(double x, double y, double tol = 1e-6) -> tuple[float, float]:
"""Transform World Mercator to GPS.
Transform WGS84 World Mercator `EPSG:3395 <https://epsg.io/3395>`_
location given as cartesian 2D coordinates x, y in meters into WGS84 decimal
degrees as longitude and latitude `EPSG:4326 <https://epsg.io/4326>`_ as
used by GPS.
Args:
x: coordinate WGS84 World Mercator
y: coordinate WGS84 World Mercator
tol: accuracy for latitude calculation
"""
# From: https://epsg.io/3395
# EPSG:3395 - World Mercator
# To: https://epsg.io/4326
# EPSG:4326 WGS84 - World Geodetic System 1984, used in GPS
# Source: Map Projections - A Working Manual
# https://pubs.usgs.gov/pp/1395/report.pdf
cdef double eccentric_2 = WGS84_ELLIPSOID_ECCENTRIC / 2.0
cdef double t = pow(M_E, (-y / WGS84_SEMI_MAJOR_AXIS)) # 7-10 p.44
cdef double e_sin_lat, latitude, latitude_prev
latitude_prev = M_PI_2 - 2.0 * atan(t) # 7-11 p.45
while True:
e_sin_lat = sin(latitude_prev) * WGS84_ELLIPSOID_ECCENTRIC
latitude = M_PI_2 - 2.0 * atan(
t * pow(((1.0 - e_sin_lat) / (1.0 + e_sin_lat)), eccentric_2)
) # 7-9 p.44
if fabs(latitude - latitude_prev) < tol:
break
latitude_prev = latitude
longitude = x / WGS84_SEMI_MAJOR_AXIS # 7-12 p.45
return longitude * DEGREES, latitude * DEGREES

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
# cython: language_level=3
# Copyright (c) 2022-2024, Manfred Moitzi
# License: MIT License
from typing import Iterator, TYPE_CHECKING, Sequence
import cython
from cpython.mem cimport PyMem_Malloc, PyMem_Free
from .vector cimport Vec3, v3_isclose, v3_sub, v3_add, v3_mul, v3_magnitude
if TYPE_CHECKING:
from ezdxf.math import UVec
__all__ = ["_LineTypeRenderer"]
LineSegment = tuple[Vec3, Vec3]
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
cdef class _LineTypeRenderer:
cdef double *dashes
cdef int dash_count
cdef readonly bint is_solid
cdef bint is_dash
cdef int current_dash
cdef double current_dash_length
def __init__(self, dashes: Sequence[float]):
cdef list _dashes = list(dashes)
cdef int i
self.dash_count = len(_dashes)
self.dashes = <double *> PyMem_Malloc(self.dash_count * sizeof(double))
for i in range(self.dash_count):
self.dashes[i] = _dashes[i]
self.is_solid = True
self.is_dash = False
self.current_dash = 0
self.current_dash_length = 0.0
if self.dash_count > 1:
self.is_solid = False
self.current_dash_length = self.dashes[0]
self.is_dash = True
def __dealloc__(self):
PyMem_Free(self.dashes)
def line_segment(self, start: UVec, end: UVec) -> Iterator[LineSegment]:
cdef Vec3 v3_start = Vec3(start)
cdef Vec3 v3_end = Vec3(end)
cdef Vec3 segment_vec, segment_dir
cdef double segment_length, dash_length
cdef list dashes = []
if self.is_solid or v3_isclose(v3_start, v3_end, REL_TOL, ABS_TOL):
yield v3_start, v3_end
return
segment_vec = v3_sub(v3_end, v3_start)
segment_length = v3_magnitude(segment_vec)
with cython.cdivision:
segment_dir = v3_mul(segment_vec, 1.0 / segment_length) # normalize
self._render_dashes(segment_length, dashes)
for dash_length in dashes:
v3_end = v3_add(v3_start, v3_mul(segment_dir, abs(dash_length)))
if dash_length > 0:
yield v3_start, v3_end
v3_start = v3_end
cdef _render_dashes(self, double length, list dashes):
if length <= self.current_dash_length:
self.current_dash_length -= length
dashes.append(length if self.is_dash else -length)
if self.current_dash_length < ABS_TOL:
self._cycle_dashes()
else:
# Avoid deep recursions!
while length > self.current_dash_length:
length -= self.current_dash_length
self._render_dashes(self.current_dash_length, dashes)
if length > 0.0:
self._render_dashes(length, dashes)
cdef _cycle_dashes(self):
with cython.cdivision:
self.current_dash = (self.current_dash + 1) % self.dash_count
self.current_dash_length = self.dashes[self.current_dash]
self.is_dash = not self.is_dash

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,835 @@
# cython: language_level=3
# Source: https://github.com/mapbox/earcut
# License: ISC License (MIT compatible)
#
# Copyright (c) 2016, Mapbox
#
# Permission to use, copy, modify, and/or distribute this software for any purpose
# with or without fee is hereby granted, provided that the above copyright notice
# and this permission notice appear in all copies.
#
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
# FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER
# TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF
# THIS SOFTWARE.
#
# Cython implementation of module ezdxf.math._mapbox_earcut.py
# Copyright (c) 2022, Manfred Moitzi
# License: MIT License
# type: ignore
from libc.math cimport fmin, fmax, fabs, INFINITY
cdef class Node:
cdef:
int i
double x
double y
int z
bint steiner
object point
Node prev
Node next
Node prev_z
Node next_z
def __cinit__(self, int i, object point):
self.i = i
self.point = point
self.x = point.x
self.y = point.y
self.z = 0
self.steiner = False
self.prev = None
self.next = None
self.prev_z = None
self.next_z = None
cdef bint equals(self, Node other):
return self.x == other.x and self.y == other.y
def node_key(Node node):
return node.x, node.y
def earcut(list exterior, list holes):
"""Implements a modified ear slicing algorithm, optimized by z-order
curve hashing and extended to handle holes, twisted polygons, degeneracies
and self-intersections in a way that doesn't guarantee correctness of
triangulation, but attempts to always produce acceptable results for
practical data.
Source: https://github.com/mapbox/earcut
Args:
exterior: outer path as list of points as objects which provide a
`x`- and a `y`-attribute
holes: list of holes, each hole is list of points, a hole with
a single points is a Steiner point
Returns:
Returns a list of triangles, each triangle is a tuple of three points,
the output points are the same objects as the input points.
"""
cdef:
Node outer_node
list triangles = []
double max_x, max_y, x, y
double min_x = 0.0
double min_y = 0.0
double inv_size = 0.0
if not exterior:
return triangles
outer_node = linked_list(exterior, 0, ccw=True)
if outer_node is None or outer_node.next is outer_node.prev:
return triangles
if holes:
outer_node = eliminate_holes(holes, len(exterior), outer_node)
# if the shape is not too simple, we'll use z-order curve hash later
# calculate polygon bbox
if len(exterior) > 80:
min_x = max_x = exterior[0].x
min_y = max_y = exterior[0].y
for point in exterior:
x = point.x
y = point.y
min_x = fmin(min_x, x)
min_y = fmin(min_y, y)
max_x = fmax(max_x, x)
max_y = fmax(max_y, y)
# min_x, min_y and inv_size are later used to transform coords into
# integers for z-order calculation
inv_size = fmax(max_x - min_x, max_y - min_y)
inv_size = 32767 / inv_size if inv_size != 0 else 0
earcut_linked(outer_node, triangles, min_x, min_y, inv_size, 0)
return triangles
cdef Node linked_list(list points, int start, bint ccw):
"""Create a circular doubly linked list from polygon points in the specified
winding order
"""
cdef:
Node last = None
int end
if ccw is (signed_area(points) < 0):
for point in points:
last = insert_node(start, point, last)
start += 1
else:
end = start + len(points)
for point in reversed(points):
last = insert_node(end, point, last)
end -= 1
# open polygon: where the 1st vertex is not coincident with the last vertex
if last and last.equals(last.next):
remove_node(last)
last = last.next
return last
cdef double signed_area(list points):
cdef:
double s = 0.0
double point_x, prev_x, point_y, prev_y
if not len(points):
return s
prev = points[-1]
prev_x = prev.x
prev_y = prev.y
for point in points:
point_x = point.x
point_y = point.y
s += (point_x - prev_x) * (point_y + prev_y)
prev_x = point_x
prev_y = point_y
# s < 0 is counter-clockwise
# s > 0 is clockwise
return s
cdef double area(Node p, Node q, Node r):
"""Returns signed area of a triangle"""
return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y)
cdef bint is_valid_diagonal(Node a, Node b):
"""Check if a diagonal between two polygon nodes is valid (lies in polygon
interior)
"""
return (
a.next.i != b.i
and a.prev.i != b.i
and not intersects_polygon(a, b) # doesn't intersect other edges
and (
locally_inside(a, b)
and locally_inside(b, a)
and middle_inside(a, b)
and (
area(a.prev, a, b.prev) or area(a, b.prev, b)
) # does not create opposite-facing sectors
or a.equals(b)
and area(a.prev, a, a.next) > 0
and area(b.prev, b, b.next) > 0
) # special zero-length case
)
cdef bint intersects_polygon(Node a, Node b):
"""Check if a polygon diagonal intersects any polygon segments"""
cdef Node p = a
while True:
if (
p.i != a.i
and p.next.i != a.i
and p.i != b.i
and p.next.i != b.i
and intersects(p, p.next, a, b)
):
return True
p = p.next
if p is a:
break
return False
cdef int sign(double num):
if num < 0.0:
return -1
if num > 0.0:
return 1
return 0
cdef bint on_segment(p: Node, q: Node, r: Node):
return fmax(p.x, r.x) >= q.x >= fmin(p.x, r.x) and fmax(
p.y, r.y
) >= q.y >= fmin(p.y, r.y)
cdef bint intersects(Node p1, Node q1, Node p2, Node q2):
"""check if two segments intersect"""
cdef:
int o1 = sign(area(p1, q1, p2))
int o2 = sign(area(p1, q1, q2))
int o3 = sign(area(p2, q2, p1))
int o4 = sign(area(p2, q2, q1))
if o1 != o2 and o3 != o4:
return True # general case
if o1 == 0 and on_segment(p1, p2, q1):
return True # p1, q1 and p2 are collinear and p2 lies on p1q1
if o2 == 0 and on_segment(p1, q2, q1):
return True # p1, q1 and q2 are collinear and q2 lies on p1q1
if o3 == 0 and on_segment(p2, p1, q2):
return True # p2, q2 and p1 are collinear and p1 lies on p2q2
if o4 == 0 and on_segment(p2, q1, q2):
return True # p2, q2 and q1 are collinear and q1 lies on p2q2
return False
cdef Node insert_node(int i, point, Node last):
"""create a node and optionally link it with previous one (in a circular
doubly linked list)
"""
cdef Node p = Node(i, point)
if last is None:
p.prev = p
p.next = p
else:
p.next = last.next
p.prev = last
last.next.prev = p
last.next = p
return p
cdef remove_node(Node p):
p.next.prev = p.prev
p.prev.next = p.next
if p.prev_z is not None:
p.prev_z.next_z = p.next_z
if p.next_z is not None:
p.next_z.prev_z = p.prev_z
cdef Node eliminate_holes(list holes, int start, Node outer_node):
"""link every hole into the outer loop, producing a single-ring polygon
without holes
"""
cdef:
list queue = []
list hole
for hole in holes:
if len(hole) < 1: # skip empty holes
continue
# hole vertices in clockwise order
_list = linked_list(hole, start, ccw=False)
if _list is _list.next:
_list.steiner = True
start += len(hole)
queue.append(get_leftmost(_list))
queue.sort(key=node_key)
# process holes from left to right
for hole_ in queue:
outer_node = eliminate_hole(hole_, outer_node)
return outer_node
cdef Node eliminate_hole(Node hole, Node outer_node):
"""Find a bridge between vertices that connects hole with an outer ring and
link it
"""
cdef:
Node bridge = find_hole_bridge(hole, outer_node)
Node bridge_reverse
if bridge is None:
return outer_node
bridge_reverse = split_polygon(bridge, hole)
# filter collinear points around the cuts
filter_points(bridge_reverse, bridge_reverse.next)
return filter_points(bridge, bridge.next)
cdef Node filter_points(Node start, Node end = None):
"""eliminate colinear or duplicate points"""
cdef:
Node p
bint again
if start is None:
return start
if end is None:
end = start
p = start
while True:
again = False
if not p.steiner and (
p.equals(p.next) or area(p.prev, p, p.next) == 0
):
remove_node(p)
p = end = p.prev
if p is p.next:
break
again = True
else:
p = p.next
if not (again or p is not end):
break
return end
# main ear slicing loop which triangulates a polygon (given as a linked list)
cdef earcut_linked(
Node ear,
list triangles,
double min_x,
double min_y,
double inv_size,
int pass_,
):
cdef:
Node stop, prev, next
bint _is_ear
if ear is None:
return
# interlink polygon nodes in z-order
if not pass_ and inv_size:
index_curve(ear, min_x, min_y, inv_size)
stop = ear
# iterate through ears, slicing them one by one
while ear.prev is not ear.next:
prev = ear.prev
next = ear.next
_is_ear = (
is_ear_hashed(ear, min_x, min_y, inv_size)
if inv_size
else is_ear(ear)
)
if _is_ear:
# cut off the triangle
triangles.append((prev.point, ear.point, next.point))
remove_node(ear)
# skipping the next vertex leads to less sliver triangles
ear = next.next
stop = next.next
continue
ear = next
# if we looped through the whole remaining polygon and can't find any more ears
if ear is stop:
# try filtering points and slicing again
if not pass_:
earcut_linked(
filter_points(ear),
triangles,
min_x,
min_y,
inv_size,
1,
)
# if this didn't work, try curing all small self-intersections locally
elif pass_ == 1:
ear = cure_local_intersections(filter_points(ear), triangles)
earcut_linked(ear, triangles, min_x, min_y, inv_size, 2)
# as a last resort, try splitting the remaining polygon into two
elif pass_ == 2:
split_ear_cut(ear, triangles, min_x, min_y, inv_size)
break
cdef bint is_ear(Node ear):
"""check whether a polygon node forms a valid ear with adjacent nodes"""
cdef:
Node a = ear.prev
Node b = ear
Node c = ear.next
Node p
double x0, x1, y0, y1
if area(a, b, c) >= 0:
return False # reflex, can't be an ear
# now make sure we don't have other points inside the potential ear
# triangle bbox
x0 = fmin(a.x, fmin(b.x, c.x))
x1 = fmax(a.x, fmax(b.x, c.x))
y0 = fmin(a.y, fmin(b.y, c.y))
y1 = fmax(a.y, fmax(b.y, c.y))
p = c.next
while p is not a:
if (
x0 <= p.x <= x1
and y0 <= p.y <= y1
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y)
and area(p.prev, p, p.next) >= 0
):
return False
p = p.next
return True
cdef bint is_ear_hashed(Node ear, double min_x, double min_y, double inv_size):
cdef:
Node a = ear.prev
Node b = ear
Node c = ear.next
double x0, x1, y0, y1, min_z, max_z
Node p, n
if area(a, b, c) >= 0:
return False # reflex, can't be an ear
# triangle bbox
x0 = fmin(a.x, fmin(b.x, c.x))
x1 = fmax(a.x, fmax(b.x, c.x))
y0 = fmin(a.y, fmin(b.y, c.y))
y1 = fmax(a.y, fmax(b.y, c.y))
# z-order range for the current triangle bbox;
min_z = z_order(x0, y0, min_x, min_y, inv_size)
max_z = z_order(x1, y1, min_x, min_y, inv_size)
p = ear.prev_z
n = ear.next_z
# look for points inside the triangle in both directions
while p and p.z >= min_z and n and n.z <= max_z:
if (
x0 <= p.x <= x1
and y0 <= p.y <= y1
and p is not a
and p is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y)
and area(p.prev, p, p.next) >= 0
):
return False
p = p.prev_z
if (
x0 <= n.x <= x1
and y0 <= n.y <= y1
and n is not a
and n is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y)
and area(n.prev, n, n.next) >= 0
):
return False
n = n.next_z
# look for remaining points in decreasing z-order
while p and p.z >= min_z:
if (
x0 <= p.x <= x1
and y0 <= p.y <= y1
and p is not a
and p is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, p.x, p.y)
and area(p.prev, p, p.next) >= 0
):
return False
p = p.prev_z
# look for remaining points in increasing z-order
while n and n.z <= max_z:
if (
x0 <= n.x <= x1
and y0 <= n.y <= y1
and n is not a
and n is not c
and point_in_triangle(a.x, a.y, b.x, b.y, c.x, c.y, n.x, n.y)
and area(n.prev, n, n.next) >= 0
):
return False
n = n.next_z
return True
cdef Node get_leftmost(Node start):
"""Find the leftmost node of a polygon ring"""
cdef:
Node p = start
Node leftmost = start
while True:
if p.x < leftmost.x or (p.x == leftmost.x and p.y < leftmost.y):
leftmost = p
p = p.next
if p is start:
break
return leftmost
cdef bint point_in_triangle(
double ax,
double ay,
double bx,
double by_,
double cx,
double cy,
double px,
double py,
):
"""Check if a point lies within a convex triangle"""
return (
(cx - px) * (ay - py) >= (ax - px) * (cy - py)
and (ax - px) * (by_ - py) >= (bx - px) * (ay - py)
and (bx - px) * (cy - py) >= (cx - px) * (by_ - py)
)
cdef bint sector_contains_sector(Node m, Node p):
"""Whether sector in vertex m contains sector in vertex p in the same
coordinates.
"""
return area(m.prev, m, p.prev) < 0 and area(p.next, m, m.next) < 0
cdef index_curve(Node start, double min_x, double min_y, double inv_size):
"""Interlink polygon nodes in z-order"""
cdef Node p = start
while True:
if p.z == 0:
p.z = z_order(p.x, p.y, min_x, min_y, inv_size)
p.prev_z = p.prev
p.next_z = p.next
p = p.next
if p is start:
break
p.prev_z.next_z = None
p.prev_z = None
sort_linked(p)
cdef int z_order(
double x0, double y0, double min_x, double min_y, double inv_size
):
"""Z-order of a point given coords and inverse of the longer side of data
bbox.
"""
# coords are transformed into non-negative 15-bit integer range
cdef:
int x = int((x0 - min_x) * inv_size)
int y = int ((y0 - min_y) * inv_size)
x = (x | (x << 8)) & 0x00FF00FF
x = (x | (x << 4)) & 0x0F0F0F0F
x = (x | (x << 2)) & 0x33333333
x = (x | (x << 1)) & 0x55555555
y = (y | (y << 8)) & 0x00FF00FF
y = (y | (y << 4)) & 0x0F0F0F0F
y = (y | (y << 2)) & 0x33333333
y = (y | (y << 1)) & 0x55555555
return x | (y << 1)
# Simon Tatham's linked list merge sort algorithm
# http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
cdef Node sort_linked(Node head):
cdef:
int in_size = 1
int num_merges, p_size, q_size, i
Node tail, p, q, e
while True:
p = head
head = None
tail = None
num_merges = 0
while p:
num_merges += 1
q = p
p_size = 0
for i in range(in_size):
p_size += 1
q = q.next_z
if not q:
break
q_size = in_size
while p_size > 0 or (q_size > 0 and q):
if p_size != 0 and (q_size == 0 or not q or p.z <= q.z):
e = p
p = p.next_z
p_size -= 1
else:
e = q
q = q.next_z
q_size -= 1
if tail:
tail.next_z = e
else:
head = e
e.prev_z = tail
tail = e
p = q
tail.next_z = None
in_size *= 2
if num_merges <= 1:
break
return head
cdef Node split_polygon(Node a, Node b):
"""Link two polygon vertices with a bridge.
If the vertices belong to the same ring, it splits polygon into two.
If one belongs to the outer ring and another to a hole, it merges it into a
single ring.
"""
cdef :
Node a2 = Node(a.i, a.point)
Node b2 = Node(b.i, b.point)
Node an = a.next
Node bp = b.prev
a.next = b
b.prev = a
a2.next = an
an.prev = a2
b2.next = a2
a2.prev = b2
bp.next = b2
b2.prev = bp
return b2
# go through all polygon nodes and cure small local self-intersections
cdef Node cure_local_intersections(Node start, list triangles):
cdef:
Node p = start
Node a, b
while True:
a = p.prev
b = p.next.next
if (
not a.equals(b)
and intersects(a, p, p.next, b)
and locally_inside(a, b)
and locally_inside(b, a)
):
triangles.append((a.point, p.point, b.point))
# remove two nodes involved
remove_node(p)
remove_node(p.next)
p = start = b
p = p.next
if p is start:
break
return filter_points(p)
cdef split_ear_cut(
Node start,
list triangles,
double min_x,
double min_y,
double inv_size,
):
"""Try splitting polygon into two and triangulate them independently"""
# look for a valid diagonal that divides the polygon into two
cdef:
Node a = start
Node b, c
while True:
b = a.next.next
while b is not a.prev:
if a.i != b.i and is_valid_diagonal(a, b):
# split the polygon in two by the diagonal
c = split_polygon(a, b)
# filter colinear points around the cuts
a = filter_points(a, a.next)
c = filter_points(c, c.next)
# run earcut on each half
earcut_linked(a, triangles, min_x, min_y, inv_size, 0)
earcut_linked(c, triangles, min_x, min_y, inv_size, 0)
return
b = b.next
a = a.next
if a is start:
break
# David Eberly's algorithm for finding a bridge between hole and outer polygon
cdef Node find_hole_bridge(Node hole, Node outer_node):
cdef:
Node p = outer_node
Node m = None
Node stop
double hx = hole.x
double hy = hole.y
double qx = -INFINITY
double mx, my, tan_min, tan
# find a segment intersected by a ray from the hole's leftmost point to the left;
# segment's endpoint with lesser x will be potential connection point
while True:
if p.y >= hy >= p.next.y != p.y:
x = p.x + (hy - p.y) * (p.next.x - p.x) / (p.next.y - p.y)
if hx >= x > qx:
qx = x
m = p if p.x < p.next.x else p.next
if x == hx:
# hole touches outer segment; pick leftmost endpoint
return m
p = p.next
if p is outer_node:
break
if m is None:
return None
# look for points inside the triangle of hole point, segment intersection and endpoint;
# if there are no points found, we have a valid connection;
# otherwise choose the point of the minimum angle with the ray as connection point
stop = m
mx = m.x
my = m.y
tan_min = INFINITY
p = m
while True:
if (
hx >= p.x >= mx
and hx != p.x
and point_in_triangle(
hx if hy < my else qx,
hy,
mx,
my,
qx if hy < my else hx,
hy,
p.x,
p.y,
)
):
tan = fabs(hy - p.y) / (hx - p.x) # tangential
if locally_inside(p, hole) and (
tan < tan_min
or (
tan == tan_min
and (
p.x > m.x
or (p.x == m.x and sector_contains_sector(m, p))
)
)
):
m = p
tan_min = tan
p = p.next
if p is stop:
break
return m
cdef bint locally_inside(Node a, Node b):
"""Check if a polygon diagonal is locally inside the polygon"""
return (
area(a, b, a.next) >= 0 and area(a, a.prev, b) >= 0
if area(a.prev, a, a.next) < 0
else area(a, b, a.prev) < 0 or area(a, a.next, b) < 0
)
cdef bint middle_inside(Node a, Node b):
"""Check if the middle point of a polygon diagonal is inside the polygon"""
cdef:
Node p = a
bint inside = False
double px = (a.x + b.x) / 2
double py = (a.y + b.y) / 2
while True:
if (
((p.y > py) != (p.next.y > py))
and p.next.y != p.y
and (px < (p.next.x - p.x) * (py - p.y) / (p.next.y - p.y) + p.x)
):
inside = not inside
p = p.next
if p is a:
break
return inside

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
# cython: language_level=3
# Copyright (c) 2020-2023, Manfred Moitzi
# License: MIT License
from .vector cimport Vec3
cdef class Matrix44:
cdef double m[16]
cdef Vec3 get_ux(self: Matrix44)
cdef Vec3 get_uy(self: Matrix44)
cdef Vec3 get_uz(self: Matrix44)
cdef inline swap(double *a, double *b):
cdef double tmp = a[0]
a[0] = b[0]
b[0] = tmp

View File

@@ -0,0 +1,676 @@
# cython: language_level=3
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
from typing import Sequence, Iterable, Tuple, TYPE_CHECKING, Iterator
from itertools import chain
import math
import numpy as np
import cython
from .vector cimport (
Vec2, Vec3, v3_normalize, v3_isclose, v3_cross, v3_dot,
)
from .vector import X_AXIS, Y_AXIS, Z_AXIS, NULLVEC
from libc.math cimport fabs, sin, cos, tan
if TYPE_CHECKING:
from ezdxf.math import UVec
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
cdef double[16] IDENTITY = [
1.0, 0.0, 0.0, 0.0,
0.0, 1.0, 0.0, 0.0,
0.0, 0.0, 1.0, 0.0,
0.0, 0.0, 0.0, 1.0
]
cdef void set_floats(double *m, object values) except *:
cdef int i = 0
for v in values:
if i < 16: # Do not write beyond array bounds
m[i] = v
i += 1
if i != 16:
raise ValueError("invalid argument count")
cdef class Matrix44:
def __cinit__(self, *args):
cdef int nargs = len(args)
if nargs == 0: # default constructor Matrix44(): fastest setup
self.m = IDENTITY # memcopy!
elif nargs == 1: # 16 numbers: slow setup
set_floats(self.m, args[0])
elif nargs == 4: # 4 rows of 4 numbers: slowest setup
set_floats(self.m, chain(*args))
else:
raise ValueError("invalid argument count: 4 row vectors or "
"iterable of 16 numbers")
def __reduce__(self):
return Matrix44, (tuple(self),)
def __getitem__(self, tuple index) -> float:
cdef int row = index[0]
cdef int col = index[1]
cdef int i = row * 4 + col
if 0 <= i < 16 and 0 <= col < 4:
return self.m[i]
else:
raise IndexError(f'index out of range: {index}')
def __setitem__(self, tuple index, double value):
cdef int row = index[0]
cdef int col = index[1]
cdef int i = row * 4 + col
if 0 <= i < 16 and 0 <= col < 4:
self.m[i] = value
else:
raise IndexError(f'index out of range: {index}')
def __iter__(self):
cdef int i
for i in range(16):
yield self.m[i]
def __repr__(self) -> str:
def format_row(row):
return "(%s)" % ", ".join(str(value) for value in row)
return "Matrix44(%s)" % \
", ".join(format_row(row) for row in self.rows())
def get_2d_transformation(self) -> Tuple[float, ...]:
cdef double *m = self.m
return m[0], m[1], 0.0, m[4], m[5], 0.0, m[12], m[13], 1.0
@staticmethod
def from_2d_transformation(components: Sequence[float]) -> Matrix44:
if len(components) != 6:
raise ValueError(
"First 2 columns of a 3x3 matrix required: m11, m12, m21, m22, m31, m32"
)
m44 = Matrix44()
m44.m[0] = components[0]
m44.m[1] = components[1]
m44.m[4] = components[2]
m44.m[5] = components[3]
m44.m[12] = components[4]
m44.m[13] = components[5]
return m44
def get_row(self, int row) -> Tuple[float, ...]:
cdef int index = row * 4
if 0 <= index < 13:
return self.m[index], self.m[index + 1], self.m[index + 2], self.m[
index + 3]
else:
raise IndexError(f'invalid row index: {row}')
def set_row(self, int row, values: Sequence[float]) -> None:
cdef Py_ssize_t count = len(values)
cdef Py_ssize_t start = row * 4
cdef Py_ssize_t i
if 0 <= row < 4:
if count > 4:
count = 4
for i in range(count):
self.m[start + i] = values[i]
else:
raise IndexError(f'invalid row index: {row}')
def get_col(self, int col) -> Tuple[float, ...]:
if 0 <= col < 4:
return self.m[col], self.m[col + 4], \
self.m[col + 8], self.m[col + 12]
else:
raise IndexError(f'invalid col index: {col}')
def set_col(self, int col, values: Sequence[float]):
cdef Py_ssize_t count = len(values)
cdef Py_ssize_t i
if 0 <= col < 4:
if count > 4:
count = 4
for i in range(count):
self.m[col + i * 4] = values[i]
else:
raise IndexError(f'invalid col index: {col}')
def rows(self) -> Iterator[Tuple[float, ...]]:
return (self.get_row(index) for index in (0, 1, 2, 3))
def columns(self) -> Iterator[Tuple[float, ...]]:
return (self.get_col(index) for index in (0, 1, 2, 3))
def copy(self) -> Matrix44:
cdef Matrix44 _copy = Matrix44()
_copy.m = self.m
return _copy
__copy__ = copy
@property
def origin(self) -> Vec3:
cdef Vec3 v = Vec3()
v.x = self.m[12]
v.y = self.m[13]
v.z = self.m[14]
return v
@origin.setter
def origin(self, v: UVec) -> None:
cdef Vec3 origin = Vec3(v)
self.m[12] = origin.x
self.m[13] = origin.y
self.m[14] = origin.z
@property
def ux(self) -> Vec3:
return self.get_ux()
cdef Vec3 get_ux(self):
cdef Vec3 v = Vec3()
v.x = self.m[0]
v.y = self.m[1]
v.z = self.m[2]
return v
@property
def uy(self) -> Vec3:
return self.get_uy()
cdef Vec3 get_uy(self):
cdef Vec3 v = Vec3()
v.x = self.m[4]
v.y = self.m[5]
v.z = self.m[6]
return v
@property
def uz(self) -> Vec3:
return self.get_uz()
cdef Vec3 get_uz(self):
cdef Vec3 v = Vec3()
v.x = self.m[8]
v.y = self.m[9]
v.z = self.m[10]
return v
@property
def is_cartesian(self) -> bool:
cdef Vec3 x_axis = v3_cross(self.get_uy(), self.get_uz())
return v3_isclose(x_axis, self.get_ux(), REL_TOL, ABS_TOL)
@property
def is_orthogonal(self) -> bool:
cdef Vec3 ux = v3_normalize(self.get_ux(), 1.0)
cdef Vec3 uy = v3_normalize(self.get_uy(), 1.0)
cdef Vec3 uz = v3_normalize(self.get_uz(), 1.0)
return fabs(v3_dot(ux, uy)) < 1e-9 and \
fabs(v3_dot(ux, uz)) < 1e-9 and \
fabs(v3_dot(uy, uz)) < 1e-9
@staticmethod
def scale(double sx, sy = None, sz = None) -> Matrix44:
cdef Matrix44 mat = Matrix44()
mat.m[0] = sx
mat.m[5] = sx if sy is None else sy
mat.m[10] = sx if sz is None else sz
return mat
@staticmethod
def translate(double dx, double dy, double dz) -> Matrix44:
cdef Matrix44 mat = Matrix44()
mat.m[12] = dx
mat.m[13] = dy
mat.m[14] = dz
return mat
@staticmethod
def x_rotate(double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
mat.m[5] = cos_a
mat.m[6] = sin_a
mat.m[9] = -sin_a
mat.m[10] = cos_a
return mat
@staticmethod
def y_rotate(double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
mat.m[0] = cos_a
mat.m[2] = -sin_a
mat.m[8] = sin_a
mat.m[10] = cos_a
return mat
@staticmethod
def z_rotate(double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
mat.m[0] = cos_a
mat.m[1] = sin_a
mat.m[4] = -sin_a
mat.m[5] = cos_a
return mat
@staticmethod
def axis_rotate(axis: UVec, double angle) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cos_a = cos(angle)
cdef double sin_a = sin(angle)
cdef double one_m_cos = 1.0 - cos_a
cdef Vec3 _axis = Vec3(axis).normalize()
cdef double x = _axis.x
cdef double y = _axis.y
cdef double z = _axis.z
mat.m[0] = x * x * one_m_cos + cos_a
mat.m[1] = y * x * one_m_cos + z * sin_a
mat.m[2] = x * z * one_m_cos - y * sin_a
mat.m[4] = x * y * one_m_cos - z * sin_a
mat.m[5] = y * y * one_m_cos + cos_a
mat.m[6] = y * z * one_m_cos + x * sin_a
mat.m[8] = x * z * one_m_cos + y * sin_a
mat.m[9] = y * z * one_m_cos - x * sin_a
mat.m[10] = z * z * one_m_cos + cos_a
return mat
@staticmethod
def xyz_rotate(double angle_x, double angle_y,
double angle_z) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double cx = cos(angle_x)
cdef double sx = sin(angle_x)
cdef double cy = cos(angle_y)
cdef double sy = sin(angle_y)
cdef double cz = cos(angle_z)
cdef double sz = sin(angle_z)
cdef double sxsy = sx * sy
cdef double cxsy = cx * sy
mat.m[0] = cy * cz
mat.m[1] = sxsy * cz + cx * sz
mat.m[2] = -cxsy * cz + sx * sz
mat.m[4] = -cy * sz
mat.m[5] = -sxsy * sz + cx * cz
mat.m[6] = cxsy * sz + sx * cz
mat.m[8] = sy
mat.m[9] = -sx * cy
mat.m[10] = cx * cy
return mat
@staticmethod
def shear_xy(double angle_x = 0, double angle_y = 0) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef double tx = tan(angle_x)
cdef double ty = tan(angle_y)
mat.m[1] = ty
mat.m[4] = tx
return mat
@staticmethod
def perspective_projection(double left, double right, double top,
double bottom, double near,
double far) -> Matrix44:
cdef Matrix44 mat = Matrix44()
mat.m[0] = (2. * near) / (right - left)
mat.m[5] = (2. * near) / (top - bottom)
mat.m[8] = (right + left) / (right - left)
mat.m[9] = (top + bottom) / (top - bottom)
mat.m[10] = -((far + near) / (far - near))
mat.m[11] = -1
mat.m[14] = -((2. * far * near) / (far - near))
return mat
@staticmethod
def perspective_projection_fov(fov: float, aspect: float, near: float,
far: float) -> Matrix44:
vrange = near * math.tan(fov / 2.)
left = -vrange * aspect
right = vrange * aspect
bottom = -vrange
top = vrange
return Matrix44.perspective_projection(left, right, bottom, top, near,
far)
@staticmethod
def chain(*matrices: Matrix44) -> Matrix44:
cdef Matrix44 transformation = Matrix44()
for matrix in matrices:
transformation *= matrix
return transformation
def __imul__(self, Matrix44 other) -> Matrix44:
cdef double[16] m1 = self.m
cdef double *m2 = other.m
self.m[0] = m1[0] * m2[0] + m1[1] * m2[4] + m1[2] * m2[8] + \
m1[3] * m2[12]
self.m[1] = m1[0] * m2[1] + m1[1] * m2[5] + m1[2] * m2[9] + \
m1[3] * m2[13]
self.m[2] = m1[0] * m2[2] + m1[1] * m2[6] + m1[2] * m2[10] + \
m1[3] * m2[14]
self.m[3] = m1[0] * m2[3] + m1[1] * m2[7] + m1[2] * m2[11] + \
m1[3] * m2[15]
self.m[4] = m1[4] * m2[0] + m1[5] * m2[4] + m1[6] * m2[8] + \
m1[7] * m2[12]
self.m[5] = m1[4] * m2[1] + m1[5] * m2[5] + m1[6] * m2[9] + \
m1[7] * m2[13]
self.m[6] = m1[4] * m2[2] + m1[5] * m2[6] + m1[6] * m2[10] + \
m1[7] * m2[14]
self.m[7] = m1[4] * m2[3] + m1[5] * m2[7] + m1[6] * m2[11] + \
m1[7] * m2[15]
self.m[8] = m1[8] * m2[0] + m1[9] * m2[4] + m1[10] * m2[8] + \
m1[11] * m2[12]
self.m[9] = m1[8] * m2[1] + m1[9] * m2[5] + m1[10] * m2[9] + \
m1[11] * m2[13]
self.m[10] = m1[8] * m2[2] + m1[9] * m2[6] + m1[10] * m2[10] + \
m1[11] * m2[14]
self.m[11] = m1[8] * m2[3] + m1[9] * m2[7] + m1[10] * m2[11] + \
m1[11] * m2[15]
self.m[12] = m1[12] * m2[0] + m1[13] * m2[4] + m1[14] * m2[8] + \
m1[15] * m2[12]
self.m[13] = m1[12] * m2[1] + m1[13] * m2[5] + m1[14] * m2[9] + \
m1[15] * m2[13]
self.m[14] = m1[12] * m2[2] + m1[13] * m2[6] + m1[14] * m2[10] + \
m1[15] * m2[14]
self.m[15] = m1[12] * m2[3] + m1[13] * m2[7] + m1[14] * m2[11] + \
m1[15] * m2[15]
return self
def __mul__(self, Matrix44 other) -> Matrix44:
cdef Matrix44 res_matrix = self.copy()
return res_matrix.__imul__(other)
# __matmul__ = __mul__ does not work!
def __matmul__(self, Matrix44 other) -> Matrix44:
cdef Matrix44 res_matrix = self.copy()
return res_matrix.__imul__(other)
def transpose(self) -> None:
swap(&self.m[1], &self.m[4])
swap(&self.m[2], &self.m[8])
swap(&self.m[3], &self.m[12])
swap(&self.m[6], &self.m[9])
swap(&self.m[7], &self.m[13])
swap(&self.m[11], &self.m[14])
def determinant(self) -> float:
cdef double *m = self.m
return m[0] * m[5] * m[10] * m[15] - m[0] * m[5] * m[11] * m[14] + \
m[0] * m[6] * m[11] * m[13] - m[0] * m[6] * m[9] * m[15] + \
m[0] * m[7] * m[9] * m[14] - m[0] * m[7] * m[10] * m[13] - \
m[1] * m[6] * m[11] * m[12] + m[1] * m[6] * m[8] * m[15] - \
m[1] * m[7] * m[8] * m[14] + m[1] * m[7] * m[10] * m[12] - \
m[1] * m[4] * m[10] * m[15] + m[1] * m[4] * m[11] * m[14] + \
m[2] * m[7] * m[8] * m[13] - m[2] * m[7] * m[9] * m[12] + \
m[2] * m[4] * m[9] * m[15] - m[2] * m[4] * m[11] * m[13] + \
m[2] * m[5] * m[11] * m[12] - m[2] * m[5] * m[8] * m[15] - \
m[3] * m[4] * m[9] * m[14] + m[3] * m[4] * m[10] * m[13] - \
m[3] * m[5] * m[10] * m[12] + m[3] * m[5] * m[8] * m[14] - \
m[3] * m[6] * m[8] * m[13] + m[3] * m[6] * m[9] * m[12]
def inverse(self) -> None:
cdef double[16] m = self.m # memcopy
cdef double det = self.determinant()
cdef double f = 1. / det
self.m[0] = (m[6] * m[11] * m[13] - m[7] * m[10] * m[13] + m[7] * m[9] *
m[14] - m[5] * m[11] * m[14] - m[6] * m[9] * m[15] + m[5] *
m[10] * m[15]) * f
self.m[1] = (m[3] * m[10] * m[13] - m[2] * m[11] * m[13] - m[3] * m[9] *
m[14] + m[1] * m[11] * m[14] + m[2] * m[9] * m[15] -
m[1] * m[10] * m[15]) * f
self.m[2] = (m[2] * m[7] * m[13] - m[3] * m[6] * m[13] + m[3] * m[5] *
m[14] - m[1] * m[7] * m[14] - m[2] * m[5] * m[15] +
m[1] * m[6] * m[15]) * f
self.m[3] = (m[3] * m[6] * m[9] - m[2] * m[7] * m[9] - m[3] * m[5] *
m[10] + m[1] * m[7] * m[10] + m[2] * m[5] * m[11] - m[1] *
m[6] * m[11]) * f
self.m[4] = (m[7] * m[10] * m[12] - m[6] * m[11] * m[12] - m[7] * m[8] *
m[14] + m[4] * m[11] * m[14] + m[6] * m[8] * m[15] -
m[4] * m[10] * m[15]) * f
self.m[5] = (m[2] * m[11] * m[12] - m[3] * m[10] * m[12] + m[3] * m[8] *
m[14] - m[0] * m[11] * m[14] - m[2] * m[8] * m[15] +
m[0] * m[10] * m[15]) * f
self.m[6] = (m[3] * m[6] * m[12] - m[2] * m[7] * m[12] - m[3] * m[4] *
m[14] + m[0] * m[7] * m[14] + m[2] * m[4] * m[15] -
m[0] * m[6] * m[15]) * f
self.m[7] = (m[2] * m[7] * m[8] - m[3] * m[6] * m[8] + m[3] * m[4] *
m[10] - m[0] * m[7] * m[10] - m[2] * m[4] * m[11] +
m[0] * m[6] * m[11]) * f
self.m[8] = (m[5] * m[11] * m[12] - m[7] * m[9] * m[12] + m[7] * m[8] *
m[13] - m[4] * m[11] * m[13] - m[5] * m[8] * m[15] +
m[4] * m[9] * m[15]) * f
self.m[9] = (m[3] * m[9] * m[12] - m[1] * m[11] * m[12] - m[3] *
m[8] * m[13] + m[0] * m[11] * m[13] + m[1] * m[8] *
m[15] - m[0] * m[9] * m[15]) * f
self.m[10] = (m[1] * m[7] * m[12] - m[3] * m[5] * m[12] + m[3] *
m[4] * m[13] - m[0] * m[7] * m[13] - m[1] * m[4] *
m[15] + m[0] * m[5] * m[15]) * f
self.m[11] = (m[3] * m[5] * m[8] - m[1] * m[7] * m[8] - m[3] * m[4] *
m[9] + m[0] * m[7] * m[9] + m[1] * m[4] * m[11] -
m[0] * m[5] * m[11]) * f
self.m[12] = (m[6] * m[9] * m[12] - m[5] * m[10] * m[12] - m[6] *
m[8] * m[13] + m[4] * m[10] * m[13] + m[5] * m[8] *
m[14] - m[4] * m[9] * m[14]) * f
self.m[13] = (m[1] * m[10] * m[12] - m[2] * m[9] * m[12] + m[2] *
m[8] * m[13] - m[0] * m[10] * m[13] - m[1] * m[8] *
m[14] + m[0] * m[9] * m[14]) * f
self.m[14] = (m[2] * m[5] * m[12] - m[1] * m[6] * m[12] - m[2] *
m[4] * m[13] + m[0] * m[6] * m[13] + m[1] * m[4] *
m[14] - m[0] * m[5] * m[14]) * f
self.m[15] = (m[1] * m[6] * m[8] - m[2] * m[5] * m[8] + m[2] * m[4] *
m[9] - m[0] * m[6] * m[9] - m[1] * m[4] * m[10] +
m[0] * m[5] * m[10]) * f
@staticmethod
def ucs(ux=X_AXIS, uy=Y_AXIS, uz=Z_AXIS, origin=NULLVEC) -> Matrix44:
cdef Matrix44 mat = Matrix44()
cdef Vec3 _ux = Vec3(ux)
cdef Vec3 _uy = Vec3(uy)
cdef Vec3 _uz = Vec3(uz)
cdef Vec3 _origin = Vec3(origin)
mat.m[0] = _ux.x
mat.m[1] = _ux.y
mat.m[2] = _ux.z
mat.m[4] = _uy.x
mat.m[5] = _uy.y
mat.m[6] = _uy.z
mat.m[8] = _uz.x
mat.m[9] = _uz.y
mat.m[10] = _uz.z
mat.m[12] = _origin.x
mat.m[13] = _origin.y
mat.m[14] = _origin.z
return mat
def transform(self, vector: UVec) -> Vec3:
cdef Vec3 res = Vec3(vector)
cdef double x = res.x
cdef double y = res.y
cdef double z = res.z
cdef double *m = self.m
res.x = x * m[0] + y * m[4] + z * m[8] + m[12]
res.y = x * m[1] + y * m[5] + z * m[9] + m[13]
res.z = x * m[2] + y * m[6] + z * m[10] + m[14]
return res
def transform_direction(self, vector: UVec, normalize=False) -> Vec3:
cdef Vec3 res = Vec3(vector)
cdef double x = res.x
cdef double y = res.y
cdef double z = res.z
cdef double *m = self.m
res.x = x * m[0] + y * m[4] + z * m[8]
res.y = x * m[1] + y * m[5] + z * m[9]
res.z = x * m[2] + y * m[6] + z * m[10]
if normalize:
return v3_normalize(res, 1.0)
else:
return res
ocs_to_wcs = transform_direction
def transform_vertices(self, vectors: Iterable[UVec]) -> Iterator[Vec3]:
cdef double *m = self.m
cdef Vec3 res
cdef double x, y, z
for vector in vectors:
res = Vec3(vector)
x = res.x
y = res.y
z = res.z
res.x = x * m[0] + y * m[4] + z * m[8] + m[12]
res.y = x * m[1] + y * m[5] + z * m[9] + m[13]
res.z = x * m[2] + y * m[6] + z * m[10] + m[14]
yield res
def fast_2d_transform(self, points: Iterable[UVec]) -> Iterator[Vec2]:
cdef double m0 = self.m[0]
cdef double m1 = self.m[1]
cdef double m4 = self.m[4]
cdef double m5 = self.m[5]
cdef double m12 = self.m[12]
cdef double m13 = self.m[13]
cdef double x, y
cdef Vec2 res
for pnt in points:
res = Vec2(pnt)
x = res.x
y = res.y
res.x = x * m0 + y * m4 + m12
res.y = x * m1 + y * m5 + m13
yield res
def transform_array_inplace(self, array: np.ndarray, ndim: int) -> None:
"""Transforms a numpy array inplace, the argument `ndim` defines the dimensions
to transform, this allows 2D/3D transformation on arrays with more columns
e.g. a polyline array which stores points as (x, y, start_width, end_width,
bulge) values.
"""
cdef int _ndim = ndim
if _ndim == 2:
assert array.shape[1] > 1
transform_2d_array_inplace(self.m, array, array.shape[0])
elif _ndim == 3:
assert array.shape[1] > 2
transform_3d_array_inplace(self.m, array, array.shape[0])
else:
raise ValueError("ndim has to be 2 or 3")
def transform_directions(
self, vectors: Iterable[UVec], normalize=False
) -> Iterator[Vec3]:
cdef double *m = self.m
cdef Vec3 res
cdef double x, y, z
cdef bint _normalize = normalize
for vector in vectors:
res = Vec3(vector)
x = res.x
y = res.y
z = res.z
res.x = x * m[0] + y * m[4] + z * m[8]
res.y = x * m[1] + y * m[5] + z * m[9]
res.z = x * m[2] + y * m[6] + z * m[10]
yield v3_normalize(res, 1.0) if _normalize else res
def ucs_vertex_from_wcs(self, wcs: Vec3) -> Vec3:
return self.ucs_direction_from_wcs(wcs - self.origin)
def ucs_direction_from_wcs(self, wcs: UVec) -> Vec3:
cdef double *m = self.m
cdef Vec3 res = Vec3(wcs)
cdef double x = res.x
cdef double y = res.y
cdef double z = res.z
res.x = x * m[0] + y * m[1] + z * m[2]
res.y = x * m[4] + y * m[5] + z * m[6]
res.z = x * m[8] + y * m[9] + z * m[10]
return res
ocs_from_wcs = ucs_direction_from_wcs
@cython.boundscheck(False)
@cython.wraparound(False)
cdef void transform_2d_array_inplace(double *m, double [:, ::1] array, Py_ssize_t size):
cdef double m0 = m[0]
cdef double m1 = m[1]
cdef double m4 = m[4]
cdef double m5 = m[5]
cdef double m12 = m[12]
cdef double m13 = m[13]
cdef double x, y
cdef Py_ssize_t i
for i in range(size):
x = array[i, 0]
y = array[i, 1]
array[i, 0] = x * m0 + y * m4 + m12
array[i, 1] = x * m1 + y * m5 + m13
@cython.boundscheck(False)
@cython.wraparound(False)
cdef void transform_3d_array_inplace(double *m, double [:, ::1] array, Py_ssize_t size):
cdef double x, y, z
cdef Py_ssize_t i
for i in range(size):
x = array[i, 0]
y = array[i, 1]
z = array[i, 2]
array[i, 0] = x * m[0] + y * m[4] + z * m[8] + m[12]
array[i, 1] = x * m[1] + y * m[5] + z * m[9] + m[13]
array[i, 2] = x * m[2] + y * m[6] + z * m[10] + m[14]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,177 @@
# cython: language_level=3
# Copyright (c) 2023-2024, Manfred Moitzi
# License: MIT License
from typing_extensions import TypeAlias
import numpy as np
import numpy.typing as npt
import cython
from .vector cimport isclose
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
NDArray: TypeAlias = npt.NDArray[np.float64]
def has_clockwise_orientation(vertices: np.ndarray) -> bool:
""" Returns True if 2D `vertices` have clockwise orientation. Ignores
z-axis of all vertices.
Args:
vertices: numpy array
Raises:
ValueError: less than 3 vertices
"""
if len(vertices) < 3:
raise ValueError('At least 3 vertices required.')
return _has_clockwise_orientation(vertices, vertices.shape[0])
@cython.boundscheck(False)
@cython.wraparound(False)
cdef bint _has_clockwise_orientation(double [:, ::1] vertices, Py_ssize_t size):
cdef Py_ssize_t index
cdef Py_ssize_t start
cdef Py_ssize_t last = size - 1
cdef double s = 0.0
cdef double p1x = vertices[0][0]
cdef double p1y = vertices[0][1]
cdef double p2x = vertices[last][0]
cdef double p2y = vertices[last][1]
# Using the same tolerance as the Python implementation:
cdef bint x_is_close = isclose(p1x, p2x, REL_TOL, ABS_TOL)
cdef bint y_is_close = isclose(p1y, p2y, REL_TOL, ABS_TOL)
if x_is_close and y_is_close:
p1x = vertices[0][0]
p1y = vertices[0][1]
start = 1
else:
p1x = vertices[last][0]
p1y = vertices[last][1]
start = 0
for index in range(start, size):
p2x = vertices[index][0]
p2y = vertices[index][1]
s += (p2x - p1x) * (p2y + p1y)
p1x = p2x
p1y = p2y
return s > 0.0
def lu_decompose(A: NDArray, m1: int, m2: int) -> tuple[NDArray, NDArray, NDArray]:
upper: np.ndarray = np.array(A, dtype=np.float64)
n: int = upper.shape[0]
lower: np.ndarray = np.zeros((n, m1), dtype=np.float64)
# Is <np.int_> better to match <int> on all platforms?
index: np.ndarray = np.zeros((n,), dtype=np.int32)
_lu_decompose_cext(upper, lower, index, n, m1, m2)
return upper, lower, index
@cython.boundscheck(False)
@cython.wraparound(False)
cdef _lu_decompose_cext(
double [:, ::1] upper,
double [:, ::1] lower,
int [::1] index,
int n,
int m1,
int m2
):
cdef int mm = m1 + m2 + 1
cdef int l = m1
cdef int i, j, k
cdef double dum
for i in range(m1):
for j in range(m1 - i, mm):
upper[i][j - l] = upper[i][j]
l -= 1
for j in range(mm - l - 1, mm):
upper[i][j] = 0.0
l = m1
for k in range(n):
dum = upper[k][0]
i = k
if l < n:
l += 1
for j in range(k + 1, l):
if abs(upper[j][0]) > abs(dum):
dum = upper[j][0]
i = j
index[k] = i + 1
if i != k:
for j in range(mm):
upper[k][j], upper[i][j] = upper[i][j], upper[k][j]
for i in range(k + 1, l):
dum = upper[i][0] / upper[k][0]
lower[k][i - k - 1] = dum
for j in range(1, mm):
upper[i][j - 1] = upper[i][j] - dum * upper[k][j]
upper[i][mm - 1] = 0.0
def solve_vector_banded_matrix(
x: NDArray,
upper: NDArray,
lower: NDArray,
index: NDArray,
m1: int,
m2: int,
) -> NDArray:
n: int = upper.shape[0]
x = np.array(x) # copy x because array x gets modified
if x.shape[0] != n:
raise ValueError(
"Item count of vector <x> has to match the row count of matrix <upper>."
)
_solve_vector_banded_matrix_cext(x, upper, lower, index, n, m1, m2)
return x
@cython.boundscheck(False)
@cython.wraparound(False)
cdef _solve_vector_banded_matrix_cext(
double [::1] x,
double [:, ::1] au,
double [:, ::1] al,
int [::1] index,
int n,
int m1,
int m2,
):
cdef int mm = m1 + m2 + 1
cdef int l = m1
cdef int i, j, k
cdef double dum
for k in range(n):
j = index[k] - 1
if j != k:
x[k], x[j] = x[j], x[k]
if l < n:
l += 1
for j in range(k + 1, l):
x[j] -= al[k][j - k - 1] * x[k]
l = 1
for i in range(n - 1, -1, -1):
dum = x[i]
for k in range(1, l):
dum -= au[i][k] * x[k + i]
x[i] = dum / au[i][0]
if l < mm:
l += 1
return x

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,49 @@
# cython: language_level=3
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
cdef bint isclose(double a, double b, double rel_tol, double abs_tol)
cdef double normalize_rad_angle(double a)
cdef double normalize_deg_angle(double a)
cdef class Vec2:
cdef readonly double x, y
# Vec2 C-functions:
cdef Vec2 v2_add(Vec2 a, Vec2 b)
cdef Vec2 v2_sub(Vec2 a, Vec2 b)
cdef Vec2 v2_mul(Vec2 a, double factor)
cdef Vec2 v2_normalize(Vec2 a, double length)
cdef double v2_dot(Vec2 a, Vec2 b)
cdef double v2_det(Vec2 a, Vec2 b)
cdef double v2_dist(Vec2 a, Vec2 b)
cdef Vec2 v2_from_angle(double angle, double length)
cdef double v2_angle_between(Vec2 a, Vec2 b) except -1000
cdef Vec2 v2_lerp(Vec2 a, Vec2 b, double factor)
cdef Vec2 v2_ortho(Vec2 a, bint ccw)
cdef Vec2 v2_project(Vec2 a, Vec2 b)
cdef bint v2_isclose(Vec2 a, Vec2 b, double rel_tol, double abs_tol)
cdef class Vec3:
cdef readonly double x, y, z
# Vec3 C-functions:
cdef Vec3 v3_add(Vec3 a, Vec3 b)
cdef Vec3 v3_sub(Vec3 a, Vec3 b)
cdef Vec3 v3_mul(Vec3 a, double factor)
cdef Vec3 v3_reverse(Vec3 a)
cdef double v3_dot(Vec3 a, Vec3 b)
cdef Vec3 v3_cross(Vec3 a, Vec3 b)
cdef double v3_magnitude_sqr(Vec3 a)
cdef double v3_magnitude(Vec3 a)
cdef double v3_dist(Vec3 a, Vec3 b)
cdef Vec3 v3_from_angle(double angle, double length)
cdef double v3_angle_between(Vec3 a, Vec3 b) except -1000
cdef double v3_angle_about(Vec3 a, Vec3 base, Vec3 target)
cdef Vec3 v3_normalize(Vec3 a, double length)
cdef Vec3 v3_lerp(Vec3 a, Vec3 b, double factor)
cdef Vec3 v3_ortho(Vec3 a, bint ccw)
cdef Vec3 v3_project(Vec3 a, Vec3 b)
cdef bint v3_isclose(Vec3 a, Vec3 b, double rel_tol, double abs_tol)

View File

@@ -0,0 +1,838 @@
# cython: language_level=3
# cython: c_api_binop_methods=True
# Copyright (c) 2020-2024, Manfred Moitzi
# License: MIT License
from typing import Iterable, List, Sequence, TYPE_CHECKING, Tuple, Iterator
from libc.math cimport fabs, sin, cos, M_PI, hypot, atan2, acos, sqrt, fmod
import random
cdef extern from "constants.h":
const double ABS_TOL
const double REL_TOL
const double M_TAU
cdef double RAD2DEG = 180.0 / M_PI
cdef double DEG2RAD = M_PI / 180.0
if TYPE_CHECKING:
from ezdxf.math import AnyVec, UVec
cdef bint isclose(double a, double b, double rel_tol, double abs_tol):
# Has to match the Python implementation!
cdef double diff = fabs(b - a)
return diff <= fabs(rel_tol * b) or \
diff <= fabs(rel_tol * a) or \
diff <= abs_tol
cdef double normalize_rad_angle(double a):
# Emulate the Python behavior of (a % math.tau)
cdef double res = fmod(a, M_TAU)
if res < 0.0:
res += M_TAU
return res
def _normalize_rad_angle(double angle):
# just for testing
return normalize_rad_angle(angle)
cdef double normalize_deg_angle(double a):
# Emulate the Python behavior of (a % 360)
cdef double res = fmod(a, 360.0)
if res < 0.0:
res += 360.0
return res
def _normalize_deg_angle(double angle):
# just for testing
return normalize_deg_angle(angle)
cdef class Vec2:
""" Immutable 2D vector.
Init:
- Vec2(vec2)
- Vec2(vec3)
- Vec2((x, y))
- Vec2((x, y, z)), ignore z-axis
- Vec2(x, y)
- Vec2(x, y, z), ignore z-axis
"""
def __cinit__(self, *args):
cdef Py_ssize_t count = len(<tuple> args)
if count == 0: # fastest init - default constructor
self.x = 0
self.y = 0
return
if count == 1:
arg = args[0]
if isinstance(arg, Vec2):
# fast init by Vec2()
self.x = (<Vec2> arg).x
self.y = (<Vec2> arg).y
return
if isinstance(arg, Vec3):
# fast init by Vec3()
self.x = (<Vec3> arg).x
self.y = (<Vec3> arg).y
return
args = arg
count = len(args)
# ignore z-axis but raise error for 4 or more arguments
if count > 3:
raise TypeError('invalid argument count')
# slow init by sequence
self.x = args[0]
self.y = args[1]
def __reduce__(self):
return Vec2, (self.x, self.y)
@property
def vec3(self) -> Vec3:
return Vec3(self)
def round(self, ndigits=None) -> Vec2:
# only used for testing
return Vec2(round(self.x, ndigits), round(self.y, ndigits))
@staticmethod
def list(items: Iterable[UVec]) -> List[Vec2]:
return list(Vec2.generate(items))
@staticmethod
def tuple(items: Iterable[UVec]) -> Sequence[Vec2]:
return tuple(Vec2.generate(items))
@staticmethod
def generate(items: Iterable[UVec]) -> Iterator[Vec2]:
return (Vec2(item) for item in items)
@staticmethod
def from_angle(double angle, double length = 1.0) -> Vec2:
return v2_from_angle(angle, length)
@staticmethod
def from_deg_angle(double angle, double length = 1.0) -> Vec2:
return v2_from_angle(angle * DEG2RAD, length)
def __str__(self) -> str:
return f'({self.x}, {self.y})'
def __repr__(self)-> str:
return "Vec2" + self.__str__()
def __len__(self) -> int:
return 2
def __hash__(self) -> int:
return hash((self.x, self.y))
def copy(self) -> Vec2:
return self # immutable
def __copy__(self) -> Vec2:
return self # immutable
def __deepcopy__(self, memodict: dict) -> Vec2:
return self # immutable
def __getitem__(self, int index) -> float:
if index == 0:
return self.x
elif index == 1:
return self.y
else:
raise IndexError(f'invalid index {index}')
def __iter__(self) -> Iterator[float]:
yield self.x
yield self.y
def __abs__(self) -> float:
return hypot(self.x, self.y)
@property
def magnitude(self) -> float:
return hypot(self.x, self.y)
@property
def is_null(self) -> bool:
return fabs(self.x) <= ABS_TOL and fabs(self.y) <= ABS_TOL
@property
def angle(self) -> float:
return atan2(self.y, self.x)
@property
def angle_deg(self) -> float:
return atan2(self.y, self.x) * RAD2DEG
def orthogonal(self, ccw: bool = True) -> Vec2:
return v2_ortho(self, ccw)
def lerp(self, other: "AnyVec", double factor = 0.5) -> Vec2:
cdef Vec2 o = Vec2(other)
return v2_lerp(self, o, factor)
def normalize(self, double length = 1.) -> Vec2:
return v2_normalize(self, length)
def project(self, other: AnyVec) -> Vec2:
cdef Vec2 o = Vec2(other)
return v2_project(self, o)
def __neg__(self) -> Vec2:
cdef Vec2 res = Vec2()
res.x = -self.x
res.y = -self.y
return res
reversed = __neg__
def __bool__(self) -> bool:
return self.x != 0 or self.y != 0
def isclose(self, other: UVec, *, double rel_tol=REL_TOL,
double abs_tol = ABS_TOL) -> bool:
cdef Vec2 o = Vec2(other)
return isclose(self.x, o.x, rel_tol, abs_tol) and \
isclose(self.y, o.y, rel_tol, abs_tol)
def __eq__(self, other: UVec) -> bool:
if not isinstance(other, Vec2):
other = Vec2(other)
return self.x == other.x and self.y == other.y
def __lt__(self, other) -> bool:
cdef Vec2 o = Vec2(other)
if self.x == o.x:
return self.y < o.y
else:
return self.x < o.x
def __add__(self, other: AnyVec) -> Vec2:
if not isinstance(other, Vec2):
other = Vec2(other)
return v2_add(self, <Vec2> other)
# __radd__ not supported for Vec2
__iadd__ = __add__ # immutable
def __sub__(self, other: AnyVec) -> Vec2:
if not isinstance(other, Vec2):
other = Vec2(other)
return v2_sub(self, <Vec2> other)
# __rsub__ not supported for Vec2
__isub__ = __sub__ # immutable
def __mul__(self, factor) -> Vec2:
if isinstance(self, Vec2):
return v2_mul(self, factor)
elif isinstance(factor, Vec2):
return v2_mul(<Vec2> factor, <double> self)
else:
return NotImplemented
# Special Cython <(3.0) feature: __rmul__ == __mul__(factor, self)
def __rmul__(self, double factor) -> Vec2:
# for Cython >= 3.0
return v2_mul(self, factor)
__imul__ = __mul__ # immutable
def __truediv__(self, double factor) -> Vec2:
return v2_mul(self, 1.0 / factor)
# __rtruediv__ not supported -> TypeError
def dot(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_dot(self, o)
def det(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_det(self, o)
def distance(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_dist(self, o)
def angle_between(self, other: AnyVec) -> float:
cdef Vec2 o = Vec2(other)
return v2_angle_between(self, o)
def rotate(self, double angle) -> Vec2:
cdef double self_angle = atan2(self.y, self.x)
cdef double magnitude = hypot(self.x, self.y)
return v2_from_angle(self_angle + angle, magnitude)
def rotate_deg(self, double angle) -> Vec2:
return self.rotate(angle * DEG2RAD)
@staticmethod
def sum(items: Iterable[Vec2]) -> Vec2:
cdef Vec2 res = Vec2()
cdef Vec2 tmp
res.x = 0.0
res.y = 0.0
for v in items:
tmp = v
res.x += tmp.x
res.y += tmp.y
return res
cdef Vec2 v2_add(Vec2 a, Vec2 b):
res = Vec2()
res.x = a.x + b.x
res.y = a.y + b.y
return res
cdef Vec2 v2_sub(Vec2 a, Vec2 b):
res = Vec2()
res.x = a.x - b.x
res.y = a.y - b.y
return res
cdef Vec2 v2_mul(Vec2 a, double factor):
res = Vec2()
res.x = a.x * factor
res.y = a.y * factor
return res
cdef double v2_dot(Vec2 a, Vec2 b):
return a.x * b.x + a.y * b.y
cdef double v2_det(Vec2 a, Vec2 b):
return a.x * b.y - a.y * b.x
cdef double v2_dist(Vec2 a, Vec2 b):
return hypot(a.x - b.x, a.y - b.y)
cdef Vec2 v2_from_angle(double angle, double length):
cdef Vec2 res = Vec2()
res.x = cos(angle) * length
res.y = sin(angle) * length
return res
cdef double v2_angle_between(Vec2 a, Vec2 b) except -1000:
cdef double cos_theta = v2_dot(v2_normalize(a, 1.0), v2_normalize(b, 1.0))
# avoid domain errors caused by floating point imprecision:
if cos_theta < -1.0:
cos_theta = -1.0
elif cos_theta > 1.0:
cos_theta = 1.0
return acos(cos_theta)
cdef Vec2 v2_normalize(Vec2 a, double length):
cdef double factor = length / hypot(a.x, a.y)
cdef Vec2 res = Vec2()
res.x = a.x * factor
res.y = a.y * factor
return res
cdef Vec2 v2_lerp(Vec2 a, Vec2 b, double factor):
cdef Vec2 res = Vec2()
res.x = a.x + (b.x - a.x) * factor
res.y = a.y + (b.y - a.y) * factor
return res
cdef Vec2 v2_ortho(Vec2 a, bint ccw):
cdef Vec2 res = Vec2()
if ccw:
res.x = -a.y
res.y = a.x
else:
res.x = a.y
res.y = -a.x
return res
cdef Vec2 v2_project(Vec2 a, Vec2 b):
cdef Vec2 uv = v2_normalize(a, 1.0)
return v2_mul(uv, v2_dot(uv, b))
cdef bint v2_isclose(Vec2 a, Vec2 b, double rel_tol, double abs_tol):
return isclose(a.x, b.x, rel_tol, abs_tol) and \
isclose(a.y, b.y, rel_tol, abs_tol)
cdef class Vec3:
""" Immutable 3D vector.
Init:
- Vec3()
- Vec3(vec3)
- Vec3(vec2)
- Vec3((x, y))
- Vec3((x, y, z))
- Vec3(x, y)
- Vec3(x, y, z)
"""
def __cinit__(self, *args):
cdef Py_ssize_t count = len(<tuple> args)
if count == 0: # fastest init - default constructor
self.x = 0
self.y = 0
self.z = 0
return
if count == 1:
arg0 = args[0]
if isinstance(arg0, Vec3):
# fast init by Vec3()
self.x = (<Vec3> arg0).x
self.y = (<Vec3> arg0).y
self.z = (<Vec3> arg0).z
return
if isinstance(arg0, Vec2):
# fast init by Vec2()
self.x = (<Vec2> arg0).x
self.y = (<Vec2> arg0).y
self.z = 0
return
args = arg0
count = len(args)
if count > 3 or count < 2:
raise TypeError('invalid argument count')
# slow init by sequence
self.x = args[0]
self.y = args[1]
if count > 2:
self.z = args[2]
else:
self.z = 0.0
def __reduce__(self):
return Vec3, self.xyz
@property
def xy(self) -> Vec3:
cdef Vec3 res = Vec3()
res.x = self.x
res.y = self.y
return res
@property
def xyz(self) -> Tuple[float, float, float]:
return self.x, self.y, self.z
@property
def vec2(self) -> Vec2:
cdef Vec2 res = Vec2()
res.x = self.x
res.y = self.y
return res
def replace(self, x: float = None, y: float = None,
z: float = None) -> Vec3:
cdef Vec3 res = Vec3()
res.x = self.x if x is None else x
res.y = self.y if y is None else y
res.z = self.z if z is None else z
return res
def round(self, ndigits: int | None = None) -> Vec3:
return Vec3(
round(self.x, ndigits),
round(self.y, ndigits),
round(self.z, ndigits),
)
@staticmethod
def list(items: Iterable[UVec]) -> List[Vec3]:
return list(Vec3.generate(items))
@staticmethod
def tuple(items: Iterable[UVec]) -> Sequence[Vec3]:
return tuple(Vec3.generate(items))
@staticmethod
def generate(items: Iterable[UVec]) -> Iterator[Vec3]:
return (Vec3(item) for item in items)
@staticmethod
def from_angle(double angle, double length = 1.0) -> Vec3:
return v3_from_angle(angle, length)
@staticmethod
def from_deg_angle(double angle, double length = 1.0) -> Vec3:
return v3_from_angle(angle * DEG2RAD, length)
@staticmethod
def random(double length = 1.0) -> Vec3:
cdef Vec3 res = Vec3()
uniform = random.uniform
res.x = uniform(-1, 1)
res.y = uniform(-1, 1)
res.z = uniform(-1, 1)
return v3_normalize(res, length)
def __str__(self) -> str:
return f'({self.x}, {self.y}, {self.z})'
def __repr__(self)-> str:
return "Vec3" + self.__str__()
def __len__(self) -> int:
return 3
def __hash__(self) -> int:
return hash(self.xyz)
def copy(self) -> Vec3:
return self # immutable
__copy__ = copy
def __deepcopy__(self, memodict: dict) -> Vec3:
return self # immutable!
def __getitem__(self, int index) -> float:
if index == 0:
return self.x
elif index == 1:
return self.y
elif index == 2:
return self.z
else:
raise IndexError(f'invalid index {index}')
def __iter__(self) -> Iterator[float]:
yield self.x
yield self.y
yield self.z
def __abs__(self) -> float:
return v3_magnitude(self)
@property
def magnitude(self) -> float:
return v3_magnitude(self)
@property
def magnitude_xy(self) -> float:
return hypot(self.x, self.y)
@property
def magnitude_square(self) -> float:
return v3_magnitude_sqr(self)
@property
def is_null(self) -> bool:
return fabs(self.x) <= ABS_TOL and fabs(self.y) <= ABS_TOL and \
fabs(self.z) <= ABS_TOL
def is_parallel(self, other: UVec, *, double rel_tol=REL_TOL,
double abs_tol = ABS_TOL) -> bool:
cdef Vec3 o = Vec3(other)
cdef Vec3 v1 = v3_normalize(self, 1.0)
cdef Vec3 v2 = v3_normalize(o, 1.0)
cdef Vec3 neg_v2 = v3_reverse(v2)
return v3_isclose(v1, v2, rel_tol, abs_tol) or \
v3_isclose(v1, neg_v2, rel_tol, abs_tol)
@property
def spatial_angle(self) -> float:
return acos(v3_dot(<Vec3> X_AXIS, v3_normalize(self, 1.0)))
@property
def spatial_angle_deg(self) -> float:
return self.spatial_angle * RAD2DEG
@property
def angle(self) -> float:
return atan2(self.y, self.x)
@property
def angle_deg(self) -> float:
return atan2(self.y, self.x) * RAD2DEG
def orthogonal(self, ccw: bool = True) -> Vec3:
return v3_ortho(self, ccw)
def lerp(self, other: UVec, double factor=0.5) -> Vec3:
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_lerp(self, <Vec3> other, factor)
def project(self, other: UVec) -> Vec3:
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_project(self, <Vec3> other)
def normalize(self, double length = 1.) -> Vec3:
return v3_normalize(self, length)
def reversed(self) -> Vec3:
return v3_reverse(self)
def __neg__(self) -> Vec3:
return v3_reverse(self)
def __bool__(self) -> bool:
return not self.is_null
def isclose(self, other: UVec, *, double rel_tol = REL_TOL,
double abs_tol = ABS_TOL) -> bool:
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_isclose(self, <Vec3> other, rel_tol, abs_tol)
def __eq__(self, other: UVec) -> bool:
if not isinstance(other, Vec3):
other = Vec3(other)
return self.x == other.x and self.y == other.y and self.z == other.z
def __lt__(self, other: UVec) -> bool:
if not isinstance(other, Vec3):
other = Vec3(other)
if self.x == (<Vec3> other).x:
return self.y < (<Vec3> other).y
else:
return self.x < (<Vec3> other).x
# Special Cython (<3.0) feature: __radd__ == __add__(other, self)
def __add__(self, other) -> Vec3:
if not isinstance(self, Vec3):
# other is the real self
return v3_add(Vec3(self), <Vec3> other)
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_add(<Vec3> self, <Vec3> other)
__radd__ = __add__ # Cython >= 3.0
__iadd__ = __add__ # immutable
# Special Cython (<3.0) feature: __rsub__ == __sub__(other, self)
def __sub__(self, other) -> Vec3:
if not isinstance(self, Vec3):
# other is the real self
return v3_sub(Vec3(self), <Vec3> other)
if not isinstance(other, Vec3):
other = Vec3(other)
return v3_sub(<Vec3> self, <Vec3> other)
def __rsub__(self, other) -> Vec3:
# for Cython >= 3.0
return v3_sub(Vec3(other), <Vec3> self)
__isub__ = __sub__ # immutable
# Special Cython <(3.0) feature: __rmul__ == __mul__(factor, self)
def __mul__(self, factor) -> Vec3:
if isinstance(factor, Vec3):
return v3_mul(<Vec3> factor, self)
return v3_mul(<Vec3> self, factor)
def __rmul__(self, double factor) -> Vec3:
# for Cython >= 3.0
return v3_mul(self, factor)
__imul__ = __mul__ # immutable
def __truediv__(self, double factor) -> Vec3:
return v3_mul(self, 1.0 / factor)
# __rtruediv__ not supported -> TypeError
@staticmethod
def sum(items: Iterable[UVec]) -> Vec3:
cdef Vec3 res = Vec3()
cdef Vec3 tmp
for v in items:
tmp = Vec3(v)
res.x += tmp.x
res.y += tmp.y
res.z += tmp.z
return res
def dot(self, other: UVec) -> float:
cdef Vec3 o = Vec3(other)
return v3_dot(self, o)
def cross(self, other: UVec) -> Vec3:
cdef Vec3 o = Vec3(other)
return v3_cross(self, o)
def distance(self, other: UVec) -> float:
cdef Vec3 o = Vec3(other)
return v3_dist(self, o)
def angle_between(self, other: UVec) -> float:
cdef Vec3 o = Vec3(other)
return v3_angle_between(self, o)
def angle_about(self, base: UVec, target: UVec) -> float:
cdef Vec3 b = Vec3(base)
cdef Vec3 t = Vec3(target)
return v3_angle_about(self, b, t)
def rotate(self, double angle) -> Vec3:
cdef double angle_ = atan2(self.y, self.x) + angle
cdef double magnitude_ = hypot(self.x, self.y)
cdef Vec3 res = Vec3.from_angle(angle_, magnitude_)
res.z = self.z
return res
def rotate_deg(self, double angle) -> Vec3:
return self.rotate(angle * DEG2RAD)
X_AXIS = Vec3(1, 0, 0)
Y_AXIS = Vec3(0, 1, 0)
Z_AXIS = Vec3(0, 0, 1)
NULLVEC = Vec3(0, 0, 0)
cdef Vec3 v3_add(Vec3 a, Vec3 b):
res = Vec3()
res.x = a.x + b.x
res.y = a.y + b.y
res.z = a.z + b.z
return res
cdef Vec3 v3_sub(Vec3 a, Vec3 b):
res = Vec3()
res.x = a.x - b.x
res.y = a.y - b.y
res.z = a.z - b.z
return res
cdef Vec3 v3_mul(Vec3 a, double factor):
res = Vec3()
res.x = a.x * factor
res.y = a.y * factor
res.z = a.z * factor
return res
cdef Vec3 v3_reverse(Vec3 a):
cdef Vec3 res = Vec3()
res.x = -a.x
res.y = -a.y
res.z = -a.z
return res
cdef double v3_dot(Vec3 a, Vec3 b):
return a.x * b.x + a.y * b.y + a.z * b.z
cdef Vec3 v3_cross(Vec3 a, Vec3 b):
res = Vec3()
res.x = a.y * b.z - a.z * b.y
res.y = a.z * b.x - a.x * b.z
res.z = a.x * b.y - a.y * b.x
return res
cdef inline double v3_magnitude_sqr(Vec3 a):
return a.x * a.x + a.y * a.y + a.z * a.z
cdef inline double v3_magnitude(Vec3 a):
return sqrt(v3_magnitude_sqr(a))
cdef double v3_dist(Vec3 a, Vec3 b):
cdef double dx = a.x - b.x
cdef double dy = a.y - b.y
cdef double dz = a.z - b.z
return sqrt(dx * dx + dy * dy + dz * dz)
cdef Vec3 v3_from_angle(double angle, double length):
cdef Vec3 res = Vec3()
res.x = cos(angle) * length
res.y = sin(angle) * length
return res
cdef double v3_angle_between(Vec3 a, Vec3 b) except -1000:
cdef double cos_theta = v3_dot(v3_normalize(a, 1.0), v3_normalize(b, 1.0))
# avoid domain errors caused by floating point imprecision:
if cos_theta < -1.0:
cos_theta = -1.0
elif cos_theta > 1.0:
cos_theta = 1.0
return acos(cos_theta)
cdef double v3_angle_about(Vec3 a, Vec3 base, Vec3 target):
cdef Vec3 x_axis = v3_normalize(v3_sub(base, v3_project(a, base)), 1.0)
cdef Vec3 y_axis = v3_normalize(v3_cross(a, x_axis), 1.0)
cdef double target_projected_x = v3_dot(x_axis, target)
cdef double target_projected_y = v3_dot(y_axis, target)
return normalize_rad_angle(atan2(target_projected_y, target_projected_x))
cdef Vec3 v3_normalize(Vec3 a, double length):
cdef double factor = length / v3_magnitude(a)
cdef Vec3 res = Vec3()
res.x = a.x * factor
res.y = a.y * factor
res.z = a.z * factor
return res
cdef Vec3 v3_lerp(Vec3 a, Vec3 b, double factor):
cdef Vec3 res = Vec3()
res.x = a.x + (b.x - a.x) * factor
res.y = a.y + (b.y - a.y) * factor
res.z = a.z + (b.z - a.z) * factor
return res
cdef Vec3 v3_ortho(Vec3 a, bint ccw):
cdef Vec3 res = Vec3()
res.z = a.z
if ccw:
res.x = -a.y
res.y = a.x
else:
res.x = a.y
res.y = -a.x
return res
cdef Vec3 v3_project(Vec3 a, Vec3 b):
cdef Vec3 uv = v3_normalize(a, 1.0)
return v3_mul(uv, v3_dot(uv, b))
cdef bint v3_isclose(Vec3 a, Vec3 b, double rel_tol, double abs_tol):
return isclose(a.x, b.x, rel_tol, abs_tol) and \
isclose(a.y, b.y, rel_tol, abs_tol) and \
isclose(a.z, b.z, rel_tol, abs_tol)
def distance(p1: UVec, p2: UVec) -> float:
cdef Vec3 a = Vec3(p1)
cdef Vec3 b = Vec3(p2)
return v3_dist(a, b)
def lerp(p1: UVec, p2: UVec, double factor = 0.5) -> Vec3:
cdef Vec3 a = Vec3(p1)
cdef Vec3 b = Vec3(p2)
return v3_lerp(a, b, factor)