1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232
|
# Copyright (c) 2025, Manfred Moitzi
# License: MIT License
import pytest
import math
from ezdxf.math import BSpline
from ezdxf.math.bspline import to_homogeneous_points, from_homogeneous_points
import numpy as np
CONTROL_POINTS = [(0, 0), (1, -1), (2, 0), (3, 2), (4, 0), (5, -2)]
WEIGHTS = [1, 2, 3, 3, 2, 1]
# visually checked in BricsCAD
EXPECTED_POINTS = [
(0.0, 0.0, 0.0),
(0.75, -0.75, 0.0),
(1.25, -0.75, 0.0),
(1.9583333333333335, 0.04166666666666663, 0.0),
(2.5, 1.0, 0.0),
(3.041666666666666, 1.583333333333333, 0.0),
(3.75, 0.5, 0.0),
(4.25, -0.5, 0.0),
(5.0, -2.0, 0.0),
]
EXPECTED_KNOTS = [
0.0,
0.0,
0.0,
0.0,
0.0,
0.3333333333333333,
0.3333333333333333,
0.6666666666666666,
0.6666666666666666,
1.0,
1.0,
1.0,
1.0,
1.0,
]
EXPECTED_POINTS_RATIONAL = [
(0.0, 0.0, 0.0),
(0.8571428571428571, -0.8571428571428571, 0.0),
(1.3333333333333333, -0.6666666666666666, 0.0),
(2.0, 0.08695652173913043, 0.0),
(2.5, 1.0, 0.0),
(2.999999999999999, 1.6521739130434785, 0.0),
(3.6666666666666665, 0.6666666666666666, 0.0),
(4.142857142857143, -0.2857142857142857, 0.0),
(5.0, -2.0, 0.0),
]
EXPECTED_WEIGHTS = [1.0, 1.75, 2.25, 2.875, 3.0, 2.8749999999999996, 2.25, 1.75, 1.0]
class TestDegreeElevationNonRationalBSpline:
@pytest.fixture(scope="class")
def result(self):
spline = BSpline(CONTROL_POINTS)
return spline.degree_elevation(1)
def test_degree_is_elevated(self, result: BSpline):
assert result.degree == 4
def test_clamped_start_and_end_points_are_preserved(self, result: BSpline):
assert result.control_points[0].isclose(CONTROL_POINTS[0])
assert result.control_points[-1].isclose(CONTROL_POINTS[-1])
def test_expected_control_points(self, result: BSpline):
assert (
all(cp.isclose(e) for cp, e in zip(result.control_points, EXPECTED_POINTS))
is True
)
def test_expected_knot_values(self, result: BSpline):
assert (
all(math.isclose(k, e) for k, e in zip(result.knots(), EXPECTED_KNOTS))
is True
)
def test_elevation_0_times(self):
spline = BSpline(CONTROL_POINTS)
assert spline.degree_elevation(0) is spline
assert spline.degree_elevation(-1) is spline
class TestDegreeElevationRationalBSpline:
@pytest.fixture(scope="class")
def result(self):
spline = BSpline(CONTROL_POINTS, weights=WEIGHTS)
return spline.degree_elevation(1)
def test_degree_is_elevated(self, result: BSpline):
assert result.degree == 4
def test_clamped_start_and_end_points_are_preserved(self, result: BSpline):
assert result.control_points[0].isclose(CONTROL_POINTS[0])
assert result.control_points[-1].isclose(CONTROL_POINTS[-1])
def test_expected_control_points(self, result: BSpline):
assert (
all(
cp.isclose(e)
for cp, e in zip(result.control_points, EXPECTED_POINTS_RATIONAL)
)
is True
)
def test_expected_knot_values(self, result: BSpline):
assert (
all(math.isclose(k, e) for k, e in zip(result.knots(), EXPECTED_KNOTS))
is True
)
def test_expected_weights(self, result: BSpline):
assert (
all(math.isclose(w, e) for w, e in zip(result.weights(), EXPECTED_WEIGHTS))
is True
)
class TestHomogeneousPoints:
def test_to_hg_points(self):
bsp = BSpline(CONTROL_POINTS, weights=WEIGHTS)
hp = to_homogeneous_points(bsp)
assert len(hp) == len(CONTROL_POINTS)
assert len(hp[0]) == 4
assert tuple(hp[1]) == (2, -2, 0, 2)
def test_to_hg_points_non_rational(self):
bsp = BSpline(CONTROL_POINTS)
hp = to_homogeneous_points(bsp)
assert len(hp) == len(CONTROL_POINTS)
assert len(hp[0]) == 4
assert tuple(hp[1]) == (1, -1, 0, 1)
def test_revert_hg_points(self):
bsp = BSpline(CONTROL_POINTS, weights=WEIGHTS)
hp = to_homogeneous_points(bsp)
points, weights = from_homogeneous_points(hp)
assert len(points) == len(CONTROL_POINTS)
assert len(weights) == len(WEIGHTS)
assert all(v.isclose(e) for v, e in zip(points, CONTROL_POINTS)) is True
assert all(math.isclose(w, e) for w, e in zip(weights, WEIGHTS)) is True
class TestPointInversion:
@pytest.fixture(scope="class")
def spline(self):
return BSpline(CONTROL_POINTS)
def test_start_point(self, spline):
assert math.isclose(0, spline.point_inversion((0, 0)))
def test_end_point(self, spline):
result = spline.point_inversion(CONTROL_POINTS[-1])
assert math.isclose(spline.max_t, result)
def test_many_points(self, spline):
t = np.linspace(0, spline.max_t, 13)
points = spline.points(t)
assert all(
math.isclose(spline.point_inversion(p), u) for p, u in zip(points, t)
)
class TestSplineMeasurement:
# measurements done in BricsCAD v25
total_length = 7.7842
mid_length = total_length / 2
mid_param = 0.6251460418169124
@pytest.fixture(scope="class")
def spline(self):
return BSpline(CONTROL_POINTS)
def test_measure_length(self, spline):
mtool = spline.measure(100)
# measurement diverges less than 1%
assert mtool.length / self.total_length > 0.99
def test_measure_length_by_0_segments(self, spline):
with pytest.raises(ValueError):
spline.measure(0)
def test_get_param_for_mid_point(self, spline):
mtool = spline.measure(100)
t = mtool.param_at(self.mid_length)
# measurement diverges less than 1%
assert 0.99 < t / self.mid_param <= 1.0
def test_params_out_of_range(self, spline):
mtool = spline.measure(100)
assert mtool.param_at(-1) == 0.0
assert mtool.param_at(100) == spline.max_t
def test_distance_from_start(self, spline):
mtool = spline.measure(100)
ratio = mtool.distance(self.mid_param) / self.mid_length
# measurement diverges less than 1%
assert 0.99 < ratio <= 1.00
def test_distance_out_of_range(self, spline):
mtool = spline.measure(100)
assert mtool.distance(-1) == 0.0
assert mtool.distance(100) == mtool.length
def test_divide(self, spline):
mtool = spline.measure(100)
params = mtool.divide(7)
assert len(params) == 6
assert params[0] != 0.0
assert params[-1] != spline.max_t
def test_extents(self, spline):
mtool = spline.measure()
assert mtool.extmin.isclose((0, -2))
assert mtool.extmax.isclose((5, 1.19263675))
def test_split_spline():
spline = BSpline(CONTROL_POINTS)
mid_t = spline.measure().divide(2)[0]
sp1, sp2 = spline.split(mid_t)
l1 = sp1.measure().length
l2 = sp2.measure().length
assert abs(l1 - l2) < 0.01
if __name__ == "__main__":
pytest.main([__file__])
|