Files
2026-10-20 14:18:35 +02:00

90 lines
3.8 KiB
Python

import math
import random
import pytest
from flight_map.curves import Cubic, interpolate
def test_spline_interpolates_samples_with_continuous_tangent() -> None:
points = [(0.0, 0.0), (1.0, 2.0), (5.0, 3.0), (6.0, -1.0)]
curves = interpolate(points)
assert len(curves) == len(points) - 1
for index, curve in enumerate(curves):
assert curve.point(0) == pytest.approx(points[index])
assert curve.point(1) == pytest.approx(points[index + 1])
for first, second in zip(curves, curves[1:], strict=False):
incoming = (first.end[0] - first.control2[0], first.end[1] - first.control2[1])
outgoing = (second.control1[0] - second.start[0], second.control1[1] - second.start[1])
cross = incoming[0] * outgoing[1] - incoming[1] * outgoing[0]
dot = incoming[0] * outgoing[0] + incoming[1] * outgoing[1]
assert abs(cross) < 1e-10 and dot > 0
# A non-collinear path must actually curve, not just encode straight lines as cubics.
assert curves[0].point(0.5) != pytest.approx((0.5, 1.0))
def test_two_points_remain_a_straight_path_and_duplicates_are_finite() -> None:
curve = interpolate([(0, 0), (9, 6)])[0]
assert curve.point(0.5) == pytest.approx((4.5, 3))
for segment in interpolate([(0, 0), (0, 0), (1, 2), (1, 2), (3, 0)]):
assert all(math.isfinite(coordinate) for p in segment.points for coordinate in p)
assert interpolate([]) == []
assert interpolate([(0, 0)]) == []
def test_curve_portion_preserves_original_shape() -> None:
curve = Cubic((-2, 0), (1, 5), (3, -5), (6, 0))
portion = curve.portion(0.2, 0.8)
for fraction in (0, 0.1, 0.5, 0.8, 1):
assert portion.point(fraction) == pytest.approx(curve.point(0.2 + fraction * 0.6))
with pytest.raises(ValueError):
curve.portion(0.8, 0.2)
def test_curve_can_enter_bounds_when_both_endpoints_are_outside() -> None:
curve = Cubic((-2, 0), (-1, 3), (1, 3), (2, 0))
rectangle = (-1.0, 1.0, 1.0, 3.0)
portions = curve.clipped(rectangle)
assert len(portions) == 1
assert portions[0].start[0] == pytest.approx(-1)
assert portions[0].end[0] == pytest.approx(1)
assert portions[0].point(0.5)[1] > 2
def test_clipping_handles_multiple_entries_tangency_and_boundary_lines() -> None:
curve = Cubic((-2, 0), (4, 0), (-4, 0), (2, 0))
rectangle = (-0.1, -1.0, 0.1, 1.0)
assert len(curve.inside_intervals(rectangle)) == 3
boundary = Cubic((-2, 0), (-1, 0), (1, 0), (2, 0))
assert len(boundary.clipped((-1, 0, 1, 1))) == 1
# Only touches the lower edge; it has no visible interval inside the rectangle.
tangent = Cubic((0, -3), (1, 1), (2, 1), (3, -3))
assert tangent.clipped((-1, 0, 4, 4)) == []
def test_random_clipping_matches_dense_curve_membership() -> None:
randomizer = random.Random(42)
rectangle = (-1.0, -1.0, 1.0, 1.0)
for _ in range(100):
points = [(randomizer.uniform(-3, 3), randomizer.uniform(-3, 3)) for _ in range(4)]
curve = Cubic(*points)
intervals = curve.inside_intervals(rectangle)
for index in range(201):
t = index / 200
x, y = curve.point(t)
inside = -1 <= x <= 1 and -1 <= y <= 1
selected = any(first - 1e-10 <= t <= last + 1e-10 for first, last in intervals)
assert selected == inside
for portion in curve.clipped(rectangle):
for index in range(51):
x, y = portion.point(index / 50)
assert -1 - 1e-8 <= x <= 1 + 1e-8 and -1 - 1e-8 <= y <= 1 + 1e-8
def test_transformation_preserves_bezier_shape() -> None:
curve = Cubic((0, 0), (1, 2), (2, -1), (3, 0))
transformed = curve.transform([2, 0, 0, -2, 10, 20])
for t in (0, 0.1, 0.6, 1):
x, y = curve.point(t)
assert transformed.point(t) == pytest.approx((2 * x + 10, -2 * y + 20))