312 lines
12 KiB
Python
312 lines
12 KiB
Python
# coding=utf-8
|
|
"""Tests du motif « Y fleche » (pytest)."""
|
|
|
|
import math
|
|
import os
|
|
import random
|
|
import re
|
|
|
|
import pytest
|
|
|
|
from pattern_core import (EdgeIndex, clip_polyline, distance_point_segment,
|
|
fill_pattern, lattice_centers, motif_polylines,
|
|
parse_color, polyline_length, polylines_to_d,
|
|
segment_intersection, unit)
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
SHAPES = os.path.join(HERE, "tests", "data", "shapes.svg")
|
|
|
|
SQUARE = [[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)]]
|
|
SQUARE_WITH_HOLE = SQUARE + [[(30.0, 30.0), (70.0, 30.0), (70.0, 70.0), (30.0, 70.0)]]
|
|
|
|
|
|
def close(a, b, tol=1e-6):
|
|
return math.isclose(a, b, abs_tol=tol)
|
|
|
|
|
|
def segments(polylines):
|
|
for polyline in polylines:
|
|
for k in range(len(polyline) - 1):
|
|
yield polyline[k], polyline[k + 1]
|
|
|
|
|
|
def dist_to_rings(point, rings):
|
|
return min(distance_point_segment(point[0], point[1], *ring[k], *ring[(k + 1) % len(ring)])
|
|
for ring in rings for k in range(len(ring)))
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Outils
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_parse_color_inkscape_integer():
|
|
assert parse_color("3014898687") == ("#b3b3b3", 1.0)
|
|
assert parse_color(str(0xFF000080)) == ("#ff0000", round(0x80 / 255, 4))
|
|
|
|
|
|
def test_parse_color_hex_and_invalid():
|
|
assert parse_color("#123456") == ("#123456", 1.0)
|
|
assert parse_color("#abc") == ("#aabbcc", 1.0)
|
|
assert parse_color("0x00ff00ff") == ("#00ff00", 1.0)
|
|
assert parse_color("pas une couleur") == ("#b3b3b3", 1.0)
|
|
|
|
|
|
def test_segment_intersection():
|
|
assert close(segment_intersection((0, 0), (10, 0), (5, -5), (5, 5)), 0.5)
|
|
assert segment_intersection((0, 0), (10, 0), (0, 1), (10, 1)) is None
|
|
assert segment_intersection((0, 0), (4, 0), (5, -5), (5, 5)) is None
|
|
|
|
|
|
def test_edge_index_contains_with_hole():
|
|
index = EdgeIndex(SQUARE_WITH_HOLE)
|
|
assert index.contains(10, 10)
|
|
assert index.contains(90, 50)
|
|
assert not index.contains(50, 50) # dans le trou
|
|
assert not index.contains(-1, 50)
|
|
assert not index.contains(50, 101)
|
|
|
|
|
|
def test_edge_index_matches_brute_force():
|
|
ring = [(50 + 40 * math.cos(a) * (1 + 0.3 * math.sin(5 * a)),
|
|
50 + 40 * math.sin(a) * (1 + 0.3 * math.sin(5 * a)))
|
|
for a in [2 * math.pi * k / 300 for k in range(300)]]
|
|
index = EdgeIndex([ring])
|
|
rng = random.Random(1)
|
|
for _ in range(2000):
|
|
x, y = rng.uniform(0, 100), rng.uniform(0, 100)
|
|
inside = False
|
|
for k in range(len(ring)):
|
|
(x1, y1), (x2, y2) = ring[k], ring[(k + 1) % len(ring)]
|
|
if (y1 > y) != (y2 > y) and x1 + (y - y1) * (x2 - x1) / (y2 - y1) > x:
|
|
inside = not inside
|
|
assert index.contains(x, y) == inside
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Motif
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_motif_structure():
|
|
polylines = motif_polylines(0, 0, 8.3, 3.3)
|
|
assert len(polylines) == 5
|
|
assert len(motif_polylines(0, 0, 8.3, 0)) == 2
|
|
chevrons = [pl for pl in polylines if len(pl) == 3 and pl[1] != (0, 0)]
|
|
stems = [pl for pl in polylines if (0, 0) in pl]
|
|
assert len(chevrons) == 3 and len(stems) == 2
|
|
# Les trois pointes sont a distance « bras » du centre.
|
|
for chevron in chevrons:
|
|
tip = chevron[1]
|
|
assert close(math.hypot(*tip), 8.3)
|
|
assert close(math.dist(chevron[0], tip), 3.3)
|
|
assert close(math.dist(chevron[2], tip), 3.3)
|
|
|
|
|
|
def test_barbs_parallel_to_other_arms():
|
|
angle = 17.0
|
|
polylines = motif_polylines(0, 0, 10, 4, angle)
|
|
arm_dirs = [unit(angle + 120 * k) for k in range(3)]
|
|
for chevron in (pl for pl in polylines if len(pl) == 3 and (0, 0) not in pl):
|
|
tip = chevron[1]
|
|
for end in (chevron[0], chevron[2]):
|
|
bx, by = (end[0] - tip[0]) / 4, (end[1] - tip[1]) / 4
|
|
# Chaque barbe est parallele a l'un des trois bras.
|
|
assert any(abs(bx * dy - by * dx) < 1e-9 for dx, dy in arm_dirs)
|
|
|
|
|
|
def test_lattice_neighbours_at_period():
|
|
centers = lattice_centers((0, 0, 50, 50), 10, 30, 0)
|
|
points = [(x, y) for _, _, x, y in centers]
|
|
for p in points[:20]:
|
|
nearest = min(math.dist(p, q) for q in points if q != p)
|
|
assert close(nearest, 10)
|
|
|
|
|
|
def test_lattice_covers_bbox():
|
|
centers = lattice_centers((0, 0, 100, 60), 7, 0, 0)
|
|
rng = random.Random(3)
|
|
points = [(x, y) for _, _, x, y in centers]
|
|
for _ in range(300):
|
|
x, y = rng.uniform(5, 95), rng.uniform(5, 55)
|
|
# Tout point du plan est a moins de periode / sqrt(3) d'un noeud.
|
|
assert min(math.dist((x, y), p) for p in points) <= 7 / math.sqrt(3) + 1e-9
|
|
|
|
|
|
def test_serpentine_order():
|
|
centers = lattice_centers((0, 0, 100, 100), 10, 0, 0)
|
|
jumps = [math.dist(a[2:], b[2:]) for a, b in zip(centers, centers[1:])]
|
|
# Dans une ligne on avance d'une periode ; le changement de ligne reste court.
|
|
assert max(jumps) < 30
|
|
|
|
|
|
def test_default_pattern_has_no_crossings():
|
|
"""Les traits de deux motifs differents ne se croisent jamais."""
|
|
motifs = fill_pattern(SQUARE, 10, mode="whole", clearance=0.0)
|
|
assert len(motifs) > 20
|
|
segs = [(m, s) for m, polylines in enumerate(motifs) for s in segments(polylines)]
|
|
for a in range(len(segs)):
|
|
ma, (p, q) = segs[a]
|
|
for b in range(a + 1, len(segs)):
|
|
mb, (r, s) = segs[b]
|
|
if ma == mb:
|
|
continue
|
|
if max(p[0], q[0]) < min(r[0], s[0]) or max(r[0], s[0]) < min(p[0], q[0]):
|
|
continue
|
|
assert segment_intersection(p, q, r, s) is None
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Decoupe et remplissage
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_clip_segment_crossing_square():
|
|
index = EdgeIndex(SQUARE)
|
|
pieces = clip_polyline([(-50, 50), (150, 50)], index)
|
|
assert len(pieces) == 1
|
|
assert close(polyline_length(pieces[0]), 100)
|
|
|
|
|
|
def test_clip_segment_through_hole():
|
|
index = EdgeIndex(SQUARE_WITH_HOLE)
|
|
pieces = clip_polyline([(-10, 50), (110, 50)], index)
|
|
assert len(pieces) == 2
|
|
assert close(sum(polyline_length(p) for p in pieces), 60)
|
|
|
|
|
|
def test_clip_keeps_polyline_continuity():
|
|
index = EdgeIndex(SQUARE)
|
|
pieces = clip_polyline([(10, 10), (50, 50), (90, 10)], index)
|
|
assert pieces == [[(10.0, 10.0), (50.0, 50.0), (90.0, 10.0)]]
|
|
|
|
|
|
def test_clip_mode_stays_inside():
|
|
motifs = fill_pattern(SQUARE_WITH_HOLE, 8, angle=12, mode="clip")
|
|
index = EdgeIndex(SQUARE_WITH_HOLE)
|
|
assert motifs
|
|
for polylines in motifs:
|
|
for p, q in segments(polylines):
|
|
mid = ((p[0] + q[0]) / 2, (p[1] + q[1]) / 2)
|
|
assert index.contains(*mid) or dist_to_rings(mid, SQUARE_WITH_HOLE) < 1e-6
|
|
for point in (p, q):
|
|
assert -1e-6 <= point[0] <= 100 + 1e-6 and -1e-6 <= point[1] <= 100 + 1e-6
|
|
|
|
|
|
def test_whole_mode_respects_clearance():
|
|
clearance = 2.5
|
|
motifs = fill_pattern(SQUARE_WITH_HOLE, 8, mode="whole", clearance=clearance)
|
|
assert motifs
|
|
for polylines in motifs:
|
|
assert len(polylines) == 5 # motif complet
|
|
for polyline in polylines:
|
|
for point in polyline:
|
|
assert dist_to_rings(point, SQUARE_WITH_HOLE) >= clearance - 1e-9
|
|
assert not (30 < point[0] < 70 and 30 < point[1] < 70)
|
|
|
|
|
|
def test_whole_mode_fewer_motifs_than_clip():
|
|
clip = fill_pattern(SQUARE, 10, mode="clip")
|
|
whole = fill_pattern(SQUARE, 10, mode="whole")
|
|
assert 0 < len(whole) < len(clip)
|
|
|
|
|
|
def test_same_origin_gives_continuous_pattern():
|
|
"""Deux formes voisines ancrees a la meme origine partagent le reseau."""
|
|
left = [[(0, 0), (50, 0), (50, 50), (0, 50)]]
|
|
right = [[(50, 0), (100, 0), (100, 50), (50, 50)]]
|
|
whole = fill_pattern([[(0, 0), (100, 0), (100, 50), (0, 50)]], 9, mode="clip")
|
|
parts = fill_pattern(left, 9, mode="clip") + fill_pattern(right, 9, mode="clip")
|
|
total = sum(polyline_length(p) for m in whole for p in m)
|
|
split = sum(polyline_length(p) for m in parts for p in m)
|
|
assert close(total, split, 1e-6)
|
|
|
|
|
|
def test_empty_inputs():
|
|
assert fill_pattern([], 10) == []
|
|
assert fill_pattern(SQUARE, 0) == []
|
|
tiny = [[(0, 0), (1, 0), (1, 1), (0, 1)]]
|
|
assert fill_pattern(tiny, 50, mode="whole") == []
|
|
|
|
|
|
def test_polylines_to_d():
|
|
d = polylines_to_d([[(0, 0), (1, 2)], [(3, 4), (5, 6), (7, 8)]], precision=1)
|
|
assert d == "M 0.0,0.0 L 1.0,2.0 M 3.0,4.0 L 5.0,6.0 L 7.0,8.0"
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Bout en bout (necessite inkex)
|
|
# --------------------------------------------------------------------------
|
|
|
|
def run_extension(tmp_path, *args):
|
|
pytest.importorskip("inkex")
|
|
from y_pattern import YArrowPattern
|
|
out = tmp_path / "out.svg"
|
|
YArrowPattern().run([*args, "--output={}".format(out), SHAPES])
|
|
return out.read_text(encoding="utf-8")
|
|
|
|
|
|
def test_end_to_end_clip(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=rect1", "--id=path2", "--period=8")
|
|
assert svg.count("Y arrow pattern") == 2
|
|
assert 'id="rect1"' in svg
|
|
|
|
|
|
def test_end_to_end_whole_removes_original(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=circle1", "--mode=whole",
|
|
"--margin=1", "--keep_original=false")
|
|
assert 'id="circle1"' not in svg
|
|
assert "Y arrow pattern" in svg
|
|
|
|
|
|
def test_end_to_end_color(tmp_path):
|
|
svg = run_extension(tmp_path, "--id=path1", "--stroke_color={}".format(0x336699FF))
|
|
assert re.search(r"stroke:#336699", svg)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
# Traductions
|
|
# --------------------------------------------------------------------------
|
|
|
|
def test_translations_up_to_date_and_complete():
|
|
"""Chaque texte du .inx et du .py a sa traduction dans chaque catalogue."""
|
|
import gettext
|
|
import i18n
|
|
|
|
msgids = [msgid for msgid, _refs in i18n.extract()]
|
|
assert "Y Arrow Pattern Fill" in msgids and "Y arrow pattern" in msgids
|
|
assert "mm" not in msgids # unites marquees translatable="no"
|
|
|
|
for language in i18n.LANGUAGES:
|
|
entries = i18n.read_po(i18n.po_path(language))
|
|
missing = [m for m in msgids if not entries.get(m, ("", False))[0]]
|
|
assert not missing, "{}.po incomplet : {}".format(language, missing)
|
|
|
|
catalog = gettext.translation(i18n.DOMAIN, i18n.LOCALE_DIR, [language])
|
|
for msgid in msgids:
|
|
assert catalog.gettext(msgid) == entries[msgid][0], "{} : .mo a recompiler (python i18n.py)".format(language)
|
|
|
|
fr = gettext.translation(i18n.DOMAIN, i18n.LOCALE_DIR, ["fr"])
|
|
assert fr.gettext("Y arrow pattern") == "Motif Y fléché"
|
|
|
|
|
|
def test_po_roundtrip(tmp_path):
|
|
import i18n
|
|
|
|
messages = [("Simple", ["a"]), ('Quote "x" and \\ back', ["a"]),
|
|
("Two\nlines", ["a"]), ("Tab\tend\n", ["a"])]
|
|
path = str(tmp_path / "xx.po")
|
|
i18n.write_po(path, "fr", messages,
|
|
{m: ("<" + m + ">", False) for m, _r in messages})
|
|
entries = i18n.read_po(path)
|
|
for msgid, _refs in messages:
|
|
assert entries[msgid] == ("<" + msgid + ">", False)
|
|
|
|
|
|
def test_inx_images_exist():
|
|
"""Les images de la boite de dialogue sont presentes (chemin relatif au .inx)."""
|
|
import xml.etree.ElementTree as ET
|
|
|
|
root = ET.parse(os.path.join(HERE, "y_pattern.inx")).getroot()
|
|
images = [elem for elem in root.iter() if elem.tag.rsplit("}", 1)[-1] == "image"]
|
|
assert images
|
|
for elem in images:
|
|
assert os.path.isfile(os.path.join(HERE, elem.text.strip())), elem.text
|