150 lines
4.9 KiB
Python
150 lines
4.9 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
"""Tests du noyau geometrique, executables sans Inkscape : pytest test_packing.py"""
|
|
|
|
import math
|
|
import random
|
|
|
|
import pytest
|
|
|
|
from packing_core import (
|
|
SpatialGrid,
|
|
distance_point_segment,
|
|
distance_to_rings,
|
|
pack_circles,
|
|
point_in_rings,
|
|
rings_bbox,
|
|
)
|
|
|
|
SQUARE = [[(0, 0), (100, 0), (100, 100), (0, 100)]]
|
|
SQUARE_WITH_HOLE = SQUARE + [[(40, 40), (60, 40), (60, 60), (40, 60)]]
|
|
|
|
|
|
# --- appartenance -----------------------------------------------------------
|
|
|
|
def test_point_inside_square():
|
|
assert point_in_rings(50, 50, SQUARE)
|
|
|
|
|
|
def test_point_outside_square():
|
|
assert not point_in_rings(150, 50, SQUARE)
|
|
assert not point_in_rings(-1, 50, SQUARE)
|
|
assert not point_in_rings(50, 200, SQUARE)
|
|
|
|
|
|
def test_hole_is_outside():
|
|
assert not point_in_rings(50, 50, SQUARE_WITH_HOLE)
|
|
assert point_in_rings(20, 20, SQUARE_WITH_HOLE)
|
|
|
|
|
|
# --- distances --------------------------------------------------------------
|
|
|
|
def test_distance_point_segment_projection_clamped():
|
|
# projection hors du segment : la distance est celle de l'extremite
|
|
assert distance_point_segment(-3, 0, 0, 0, 10, 0) == pytest.approx(3)
|
|
assert distance_point_segment(5, 4, 0, 0, 10, 0) == pytest.approx(4)
|
|
|
|
|
|
def test_distance_to_rings_center_of_square():
|
|
assert distance_to_rings(50, 50, SQUARE) == pytest.approx(50)
|
|
assert distance_to_rings(5, 50, SQUARE) == pytest.approx(5)
|
|
|
|
|
|
def test_distance_to_rings_accounts_for_hole():
|
|
# au centre du carre troue, le bord le plus proche est celui du trou
|
|
assert distance_to_rings(50, 50, SQUARE_WITH_HOLE) == pytest.approx(10)
|
|
|
|
|
|
def test_rings_bbox():
|
|
assert rings_bbox(SQUARE) == (0, 0, 100, 100)
|
|
assert rings_bbox([]) is None
|
|
|
|
|
|
# --- grille spatiale --------------------------------------------------------
|
|
|
|
def test_grid_returns_near_circle_only():
|
|
grid = SpatialGrid(cell_size=10)
|
|
grid.add(0, 0, 5)
|
|
assert list(grid.neighbors(3, 3, reach=10)) == [(0, 0, 5)]
|
|
assert list(grid.neighbors(500, 500, reach=10)) == []
|
|
assert len(grid) == 1
|
|
|
|
|
|
# --- packing ----------------------------------------------------------------
|
|
|
|
def _invariants(circles, rings, min_radius, max_radius, gap, margin):
|
|
eps = 1e-9
|
|
for x, y, r in circles:
|
|
assert min_radius - eps <= r <= max_radius + eps
|
|
assert point_in_rings(x, y, rings)
|
|
assert distance_to_rings(x, y, rings) >= r + margin - eps
|
|
for i, (x1, y1, r1) in enumerate(circles):
|
|
for x2, y2, r2 in circles[i + 1:]:
|
|
assert math.hypot(x1 - x2, y1 - y2) >= r1 + r2 + gap - eps
|
|
|
|
|
|
def test_pack_circles_respects_invariants():
|
|
circles = pack_circles(SQUARE, min_radius=2, max_radius=15, gap=1,
|
|
margin=1, attempts=3000, rng=random.Random(42))
|
|
assert len(circles) > 20
|
|
_invariants(circles, SQUARE, 2, 15, 1, 1)
|
|
|
|
|
|
def test_pack_circles_avoids_hole():
|
|
circles = pack_circles(SQUARE_WITH_HOLE, min_radius=2, max_radius=15,
|
|
gap=0.5, margin=0.5, attempts=3000,
|
|
rng=random.Random(7))
|
|
assert circles
|
|
_invariants(circles, SQUARE_WITH_HOLE, 2, 15, 0.5, 0.5)
|
|
for x, y, _ in circles:
|
|
assert not (40 < x < 60 and 40 < y < 60)
|
|
|
|
|
|
def test_same_seed_gives_same_result():
|
|
args = dict(min_radius=2, max_radius=12, gap=0.5, margin=0.5, attempts=800)
|
|
first = pack_circles(SQUARE, rng=random.Random(123), **args)
|
|
second = pack_circles(SQUARE, rng=random.Random(123), **args)
|
|
assert first == second
|
|
|
|
|
|
def test_max_circles_caps_output():
|
|
circles = pack_circles(SQUARE, min_radius=1, max_radius=10, gap=0.2,
|
|
margin=0.2, attempts=5000, max_circles=10,
|
|
rng=random.Random(1))
|
|
assert len(circles) == 10
|
|
|
|
|
|
def test_larger_gap_places_fewer_circles():
|
|
args = dict(min_radius=2, max_radius=10, margin=0.5, attempts=2000)
|
|
tight = pack_circles(SQUARE, gap=0.0, rng=random.Random(5), **args)
|
|
loose = pack_circles(SQUARE, gap=6.0, rng=random.Random(5), **args)
|
|
assert len(loose) < len(tight)
|
|
|
|
|
|
def test_empty_rings():
|
|
assert pack_circles([], min_radius=1, max_radius=5) == []
|
|
|
|
|
|
# --- bout en bout dans Inkscape (saute si inkex absent) ---------------------
|
|
|
|
def test_extension_runs_on_sample_svg(tmp_path):
|
|
pytest.importorskip("inkex")
|
|
from circle_packing import CirclePacking
|
|
|
|
svg = tmp_path / "in.svg"
|
|
svg.write_text(
|
|
'<svg xmlns="http://www.w3.org/2000/svg" width="100mm" height="100mm" '
|
|
'viewBox="0 0 100 100">'
|
|
'<rect id="r" x="10" y="10" width="80" height="80" '
|
|
'style="fill:#3366cc;stroke:none"/></svg>',
|
|
encoding="utf-8")
|
|
|
|
ext = CirclePacking()
|
|
ext.run(["--id=r", "--unit=px", "--min_radius=2", "--max_radius=10",
|
|
"--attempts=2000", "--seed=1", str(svg)],
|
|
output=str(tmp_path / "out.svg"))
|
|
|
|
out = (tmp_path / "out.svg").read_text(encoding="utf-8")
|
|
assert out.count("<circle") > 10
|
|
assert "#3366cc" in out
|