freeroute/tests/geometry/test_polyline.py
Ryan Malloy dffa54a43a Add shove_segment geometric primitive
Displaces one segment of a trace polyline perpendicular by a given
offset, keeping the trace orthogonal and its two pad endpoints fixed: a
jog corner is inserted when the moved segment is first or last, and
interior neighbours simply resize. Collinear/duplicate corners are
dropped. This is the foundation for shoving a trace aside to make room
for another. Unit-tested for single-segment, interior-segment, and
orthogonality-preservation cases.
2026-07-13 02:43:09 -06:00

82 lines
2.7 KiB
Python

"""Tests for Polyline / PolylineShape (the swept trace copper)."""
from __future__ import annotations
from freeroute.geometry import (
IntBox,
IntPoint,
Polyline,
PolylineShape,
segment_box,
shove_segment,
)
def test_polyline_segments():
pl = Polyline([IntPoint(0, 0), IntPoint(10, 0), IntPoint(10, 10)])
segs = list(pl.segments())
assert len(segs) == 2
assert pl.corner_count() == 3
assert not pl.is_empty()
assert Polyline([IntPoint(0, 0)]).is_empty()
def test_segment_box_horizontal_is_flush_in_x():
box = segment_box(IntPoint(0, 0), IntPoint(100, 0), 10)
# flush in the travel direction (x), offset by half-width in y
assert box == IntBox(0, -10, 100, 10)
def test_segment_box_vertical_is_flush_in_y():
box = segment_box(IntPoint(0, 0), IntPoint(0, 100), 10)
assert box == IntBox(-10, 0, 10, 100)
def test_polyline_shape_tiles_include_corner_squares():
pl = Polyline([IntPoint(0, 0), IntPoint(100, 0), IntPoint(100, 100)])
tiles = PolylineShape(pl, 10).tiles()
# 2 segment boxes + 1 interior-corner square
assert len(tiles) == 3
# the corner square is centred on the turn at (100, 0)
assert IntBox(90, -10, 110, 10) in tiles
def test_polyline_shape_bounding_box():
pl = Polyline([IntPoint(0, 0), IntPoint(100, 0)])
assert PolylineShape(pl, 5).bounding_box() == IntBox(-5, -5, 105, 5)
def test_diagonal_segment_box_is_conservative_bbox():
box = segment_box(IntPoint(0, 0), IntPoint(50, 50), 10)
assert box == IntBox(-10, -10, 60, 60)
# --- shove primitive --------------------------------------------------------
def test_shove_single_segment_inserts_jogs_and_keeps_pads():
# a horizontal pad-to-pad trace shoved up by 30 keeps its endpoints
corners = [IntPoint(0, 0), IntPoint(100, 0)]
out = shove_segment(corners, 0, 0, 30)
assert [(p.x, p.y) for p in out] == [(0, 0), (0, 30), (100, 30), (100, 0)]
assert out[0] == corners[0] and out[-1] == corners[1] # pads fixed
def test_shove_interior_segment_resizes_neighbours():
# the middle horizontal run of a U shoved up 20; verticals resize, pads fixed
corners = [IntPoint(0, 0), IntPoint(0, 50), IntPoint(100, 50), IntPoint(100, 0)]
out = shove_segment(corners, 1, 0, 20)
assert [(p.x, p.y) for p in out] == [(0, 0), (0, 70), (100, 70), (100, 0)]
def test_shove_stays_orthogonal():
corners = [IntPoint(0, 0), IntPoint(0, 40), IntPoint(80, 40), IntPoint(80, 0)]
out = shove_segment(corners, 1, 0, 15)
for a, b in zip(out, out[1:], strict=False):
assert a.x == b.x or a.y == b.y # every segment axis-aligned
def test_shove_out_of_range_is_noop():
corners = [IntPoint(0, 0), IntPoint(100, 0)]
assert shove_segment(corners, 5, 0, 30) == corners