masque/masque/test/test_shape_advanced.py

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