#!/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(
'',
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(" 10
assert "#3366cc" in out