199 lines
6.8 KiB
Python
199 lines
6.8 KiB
Python
from pathlib import Path
|
|
import pytest
|
|
import numpy
|
|
from numpy.testing import assert_equal, assert_allclose
|
|
from numpy import pi
|
|
|
|
from ..shapes import Arc, Ellipse, Circle, Polygon, Path as MPath, Text, PolyCollection
|
|
from ..error import PatternError
|
|
|
|
|
|
# 1. Text shape tests
|
|
def test_text_to_polygons() -> None:
|
|
pytest.importorskip("freetype")
|
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf"
|
|
if not Path(font_path).exists():
|
|
pytest.skip("Font file not found")
|
|
|
|
t = Text("Hi", height=10, font_path=font_path)
|
|
polys = t.to_polygons()
|
|
assert len(polys) > 0
|
|
assert all(isinstance(p, Polygon) for p in polys)
|
|
|
|
# Check that it advances
|
|
# Character 'H' and 'i' should have different vertices
|
|
# Each character is a set of polygons. We check the mean x of vertices for each character.
|
|
char_x_means = [p.vertices[:, 0].mean() for p in polys]
|
|
assert len(set(char_x_means)) >= 2
|
|
|
|
|
|
def test_text_bounds_and_normalized_form() -> None:
|
|
pytest.importorskip("freetype")
|
|
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuMathTeXGyre.ttf"
|
|
if not Path(font_path).exists():
|
|
pytest.skip("Font file not found")
|
|
|
|
text = Text("Hi", height=10, font_path=font_path)
|
|
_intrinsic, extrinsic, ctor = text.normalized_form(5)
|
|
normalized = ctor()
|
|
|
|
assert extrinsic[1] == 2
|
|
assert normalized.height == 5
|
|
|
|
bounds = text.get_bounds_single()
|
|
assert bounds is not None
|
|
assert numpy.isfinite(bounds).all()
|
|
assert numpy.all(bounds[1] > bounds[0])
|
|
|
|
|
|
def test_text_mirroring_affects_comparison() -> None:
|
|
text = Text("A", height=10, font_path="dummy.ttf")
|
|
mirrored = Text("A", height=10, font_path="dummy.ttf", mirrored=True)
|
|
|
|
assert text != mirrored
|
|
assert (text < mirrored) != (mirrored < text)
|
|
|
|
|
|
# 2. Manhattanization tests
|
|
def test_manhattanize() -> None:
|
|
pytest.importorskip("float_raster")
|
|
pytest.importorskip("skimage.measure")
|
|
# Diamond shape
|
|
poly = Polygon([[0, 5], [5, 10], [10, 5], [5, 0]])
|
|
grid = numpy.arange(0, 11, 1)
|
|
|
|
manhattan_polys = poly.manhattanize(grid, grid)
|
|
assert len(manhattan_polys) >= 1
|
|
for mp in manhattan_polys:
|
|
# Check that all edges are axis-aligned
|
|
dv = numpy.diff(mp.vertices, axis=0)
|
|
# For each segment, either dx or dy must be zero
|
|
assert numpy.all((dv[:, 0] == 0) | (dv[:, 1] == 0))
|
|
|
|
|
|
# 3. Comparison and Sorting tests
|
|
def test_shape_comparisons() -> None:
|
|
c1 = Circle(radius=10)
|
|
c2 = Circle(radius=20)
|
|
assert c1 < c2
|
|
assert not (c2 < c1)
|
|
|
|
p1 = Polygon([[0, 0], [10, 0], [10, 10]])
|
|
p2 = Polygon([[0, 0], [10, 0], [10, 11]]) # Different vertex
|
|
assert p1 < p2
|
|
|
|
# Different types
|
|
assert c1 < p1 or p1 < c1
|
|
assert (c1 < p1) != (p1 < c1)
|
|
|
|
|
|
# 4. Arc/Path Edge Cases
|
|
def test_arc_edge_cases() -> None:
|
|
# Wrapped arc (> 360 deg)
|
|
a = Arc(radii=(10, 10), angles=(0, 3 * pi), width=2)
|
|
a.to_polygons(num_vertices=64)
|
|
# Should basically be a ring
|
|
bounds = a.get_bounds_single()
|
|
assert_allclose(bounds, [[-11, -11], [11, 11]], atol=1e-10)
|
|
|
|
|
|
def test_rotated_ellipse_bounds_match_polygonized_geometry() -> None:
|
|
ellipse = Ellipse(radii=(10, 20), rotation=pi / 4, offset=(100, 200))
|
|
bounds = ellipse.get_bounds_single()
|
|
poly_bounds = ellipse.to_polygons(num_vertices=8192)[0].get_bounds_single()
|
|
assert_allclose(bounds, poly_bounds, atol=1e-3)
|
|
|
|
|
|
def test_rotated_arc_bounds_match_polygonized_geometry() -> None:
|
|
arc = Arc(radii=(10, 20), angles=(0, pi), width=2, rotation=pi / 4, offset=(100, 200))
|
|
bounds = arc.get_bounds_single()
|
|
poly_bounds = arc.to_polygons(num_vertices=8192)[0].get_bounds_single()
|
|
assert_allclose(bounds, poly_bounds, atol=1e-3)
|
|
|
|
|
|
def test_curve_polygonizers_clamp_large_max_arclen() -> None:
|
|
for shape in (
|
|
Circle(radius=10),
|
|
Ellipse(radii=(10, 20)),
|
|
Arc(radii=(10, 20), angles=(0, 1), width=2),
|
|
):
|
|
polys = shape.to_polygons(num_vertices=None, max_arclen=1e9)
|
|
assert len(polys) == 1
|
|
assert len(polys[0].vertices) >= 3
|
|
|
|
|
|
def test_path_edge_cases() -> None:
|
|
# Zero-length segments
|
|
p = MPath(vertices=[[0, 0], [0, 0], [10, 0]], width=2)
|
|
polys = p.to_polygons()
|
|
assert len(polys) == 1
|
|
assert_equal(polys[0].get_bounds_single(), [[0, -1], [10, 1]])
|
|
|
|
|
|
# 5. PolyCollection with holes
|
|
def test_poly_collection_holes() -> None:
|
|
# Outer square, inner square hole
|
|
# PolyCollection doesn't explicitly support holes, but its constituents (Polygons) do?
|
|
# wait, Polygon in masque is just a boundary. Holes are usually handled by having multiple
|
|
# polygons or using specific winding rules.
|
|
# masque.shapes.Polygon doc says "specify an implicitly-closed boundary".
|
|
# Pyclipper is used in connectivity.py for holes.
|
|
|
|
# Let's test PolyCollection with multiple polygons
|
|
verts = [
|
|
[0, 0],
|
|
[10, 0],
|
|
[10, 10],
|
|
[0, 10], # Poly 1
|
|
[2, 2],
|
|
[2, 8],
|
|
[8, 8],
|
|
[8, 2], # Poly 2
|
|
]
|
|
offsets = [0, 4]
|
|
pc = PolyCollection(verts, offsets)
|
|
polys = pc.to_polygons()
|
|
assert len(polys) == 2
|
|
assert_equal(polys[0].vertices, [[0, 0], [10, 0], [10, 10], [0, 10]])
|
|
assert_equal(polys[1].vertices, [[2, 2], [2, 8], [8, 8], [8, 2]])
|
|
|
|
|
|
def test_poly_collection_constituent_empty() -> None:
|
|
# One real triangle, one "empty" polygon (0 vertices), one real square
|
|
# Note: Polygon requires 3 vertices, so "empty" here might mean just some junk
|
|
# that to_polygons should handle.
|
|
# Actually PolyCollection doesn't check vertex count per polygon.
|
|
verts = [
|
|
[0, 0],
|
|
[1, 0],
|
|
[0, 1], # Tri
|
|
# Empty space
|
|
[10, 10],
|
|
[11, 10],
|
|
[11, 11],
|
|
[10, 11], # Square
|
|
]
|
|
offsets = [0, 3, 3] # Index 3 is start of "empty", Index 3 is also start of Square?
|
|
# No, offsets should be strictly increasing or handle 0-length slices.
|
|
# vertex_slices uses zip(offsets, chain(offsets[1:], [len(verts)]))
|
|
# if offsets = [0, 3, 3], slices are [0:3], [3:3], [3:7]
|
|
offsets = [0, 3, 3]
|
|
pc = PolyCollection(verts, offsets)
|
|
# Polygon(vertices=[]) will fail because of the setter check.
|
|
# Let's see if pc.to_polygons() handles it.
|
|
# It calls Polygon(vertices=vv) for each slice.
|
|
# slice [3:3] gives empty vv.
|
|
with pytest.raises(PatternError):
|
|
pc.to_polygons()
|
|
|
|
|
|
def test_poly_collection_valid() -> None:
|
|
verts = [[0, 0], [1, 0], [0, 1], [10, 10], [11, 10], [11, 11], [10, 11]]
|
|
offsets = [0, 3]
|
|
pc = PolyCollection(verts, offsets)
|
|
assert len(pc.to_polygons()) == 2
|
|
shapes = [Circle(radius=20), Circle(radius=10), Polygon([[0, 0], [10, 0], [10, 10]]), Ellipse(radii=(5, 5))]
|
|
sorted_shapes = sorted(shapes)
|
|
assert len(sorted_shapes) == 4
|
|
# Just verify it doesn't crash and is stable
|
|
assert sorted(sorted_shapes) == sorted_shapes
|