2026-02-16 20:48:15 -08:00
|
|
|
# ruff: noqa: PLC0415
|
2026-02-16 17:41:58 -08:00
|
|
|
import pytest
|
|
|
|
|
import numpy
|
|
|
|
|
from numpy.testing import assert_allclose
|
|
|
|
|
from masque.pattern import Pattern
|
|
|
|
|
from masque.shapes.polygon import Polygon
|
|
|
|
|
from masque.repetition import Grid
|
|
|
|
|
from masque.library import Library
|
|
|
|
|
|
|
|
|
|
def test_layer_as_polygons_basic() -> None:
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
pat.polygon((1, 0), [[0, 0], [1, 0], [1, 1], [0, 1]])
|
|
|
|
|
|
|
|
|
|
polys = pat.layer_as_polygons((1, 0), flatten=False)
|
|
|
|
|
assert len(polys) == 1
|
|
|
|
|
assert isinstance(polys[0], Polygon)
|
|
|
|
|
assert_allclose(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]])
|
|
|
|
|
|
|
|
|
|
def test_layer_as_polygons_repetition() -> None:
|
|
|
|
|
pat = Pattern()
|
|
|
|
|
rep = Grid(a_vector=(2, 0), a_count=2)
|
|
|
|
|
pat.polygon((1, 0), [[0, 0], [1, 0], [1, 1], [0, 1]], repetition=rep)
|
|
|
|
|
|
|
|
|
|
polys = pat.layer_as_polygons((1, 0), flatten=False)
|
|
|
|
|
assert len(polys) == 2
|
|
|
|
|
# First polygon at (0,0)
|
|
|
|
|
assert_allclose(polys[0].vertices, [[0, 0], [1, 0], [1, 1], [0, 1]])
|
|
|
|
|
# Second polygon at (2,0)
|
|
|
|
|
assert_allclose(polys[1].vertices, [[2, 0], [3, 0], [3, 1], [2, 1]])
|
|
|
|
|
|
|
|
|
|
def test_layer_as_polygons_flatten() -> None:
|
|
|
|
|
lib = Library()
|
|
|
|
|
|
|
|
|
|
child = Pattern()
|
|
|
|
|
child.polygon((1, 0), [[0, 0], [1, 0], [1, 1]])
|
|
|
|
|
lib['child'] = child
|
|
|
|
|
|
|
|
|
|
parent = Pattern()
|
|
|
|
|
parent.ref('child', offset=(10, 10), rotation=numpy.pi/2)
|
|
|
|
|
|
|
|
|
|
polys = parent.layer_as_polygons((1, 0), flatten=True, library=lib)
|
|
|
|
|
assert len(polys) == 1
|
|
|
|
|
# Original child at (0,0) with rot pi/2 is still at (0,0) in its own space?
|
|
|
|
|
# No, ref.as_pattern(child) will apply the transform.
|
|
|
|
|
# Child (0,0), (1,0), (1,1) rotated pi/2 around (0,0) -> (0,0), (0,1), (-1,1)
|
|
|
|
|
# Then offset by (10,10) -> (10,10), (10,11), (9,11)
|
|
|
|
|
|
|
|
|
|
# Let's verify the vertices
|
|
|
|
|
expected = numpy.array([[10, 10], [10, 11], [9, 11]])
|
|
|
|
|
assert_allclose(polys[0].vertices, expected, atol=1e-10)
|
|
|
|
|
|
|
|
|
|
def test_boolean_import_error() -> None:
|
|
|
|
|
from masque import boolean
|
|
|
|
|
# If pyclipper is not installed, this should raise ImportError
|
|
|
|
|
try:
|
|
|
|
|
import pyclipper # noqa: F401
|
|
|
|
|
pytest.skip("pyclipper is installed, cannot test ImportError")
|
|
|
|
|
except ImportError:
|
|
|
|
|
with pytest.raises(ImportError, match="Boolean operations require 'pyclipper'"):
|
|
|
|
|
boolean([], [], operation='union')
|
|
|
|
|
|
|
|
|
|
def test_polygon_boolean_shortcut() -> None:
|
|
|
|
|
poly = Polygon([[0, 0], [1, 0], [1, 1]])
|
|
|
|
|
# This should also raise ImportError if pyclipper is missing
|
|
|
|
|
try:
|
|
|
|
|
import pyclipper # noqa: F401
|
|
|
|
|
pytest.skip("pyclipper is installed")
|
|
|
|
|
except ImportError:
|
|
|
|
|
with pytest.raises(ImportError, match="Boolean operations require 'pyclipper'"):
|
|
|
|
|
poly.boolean(poly)
|
|
|
|
|
|
|
|
|
|
def test_bridge_holes() -> None:
|
|
|
|
|
from masque.utils.boolean import _bridge_holes
|
|
|
|
|
|
|
|
|
|
# Outer: 10x10 square
|
|
|
|
|
outer = numpy.array([[0, 0], [10, 0], [10, 10], [0, 10]])
|
|
|
|
|
# Hole: 2x2 square in the middle
|
|
|
|
|
hole = numpy.array([[4, 4], [6, 4], [6, 6], [4, 6]])
|
|
|
|
|
|
|
|
|
|
bridged = _bridge_holes(outer, [hole])
|
|
|
|
|
|
|
|
|
|
# We expect more vertices than outer + hole
|
|
|
|
|
# Original outer has 4, hole has 4. Bridge adds 2 (to hole) and 2 (back to outer) + 1 to close hole loop?
|
|
|
|
|
# Our implementation:
|
|
|
|
|
# 1. outer up to bridge edge (best_edge_idx)
|
|
|
|
|
# 2. bridge point on outer
|
|
|
|
|
# 3. hole reordered starting at max X
|
|
|
|
|
# 4. close hole loop (repeat max X)
|
|
|
|
|
# 5. bridge point on outer again
|
|
|
|
|
# 6. rest of outer
|
|
|
|
|
|
|
|
|
|
# max X of hole is 6 at (6,4) or (6,6). argmax will pick first one.
|
|
|
|
|
# hole vertices: [4,4], [6,4], [6,6], [4,6]. argmax(x) is index 1: (6,4)
|
|
|
|
|
# roll hole to start at (6,4): [6,4], [6,6], [4,6], [4,4]
|
|
|
|
|
|
|
|
|
|
# intersection of ray from (6,4) to right:
|
|
|
|
|
# edges of outer: (0,0)-(10,0), (10,0)-(10,10), (10,10)-(0,10), (0,10)-(0,0)
|
|
|
|
|
# edge (10,0)-(10,10) spans y=4.
|
|
|
|
|
# intersection at (10,4). best_edge_idx = 1 (edge from index 1 to 2)
|
|
|
|
|
|
|
|
|
|
# vertices added:
|
|
|
|
|
# outer[0:2]: (0,0), (10,0)
|
|
|
|
|
# bridge pt: (10,4)
|
|
|
|
|
# hole: (6,4), (6,6), (4,6), (4,4)
|
|
|
|
|
# hole close: (6,4)
|
|
|
|
|
# bridge pt back: (10,4)
|
|
|
|
|
# outer[2:]: (10,10), (0,10)
|
|
|
|
|
|
|
|
|
|
expected_len = 11
|
|
|
|
|
assert len(bridged) == expected_len
|
|
|
|
|
|
|
|
|
|
# verify it wraps around the hole and back
|
|
|
|
|
# index 2 is bridge_pt
|
|
|
|
|
assert_allclose(bridged[2], [10, 4])
|
|
|
|
|
# index 3 is hole reordered max X
|
|
|
|
|
assert_allclose(bridged[3], [6, 4])
|
|
|
|
|
# index 7 is hole closed at max X
|
|
|
|
|
assert_allclose(bridged[7], [6, 4])
|
|
|
|
|
# index 8 is bridge_pt back
|
|
|
|
|
assert_allclose(bridged[8], [10, 4])
|