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.
This commit is contained in:
parent
0bee385e71
commit
dffa54a43a
@ -24,7 +24,7 @@ from .line import Line
|
|||||||
from .point import IntPoint, Point, RationalPoint, point, rational_point
|
from .point import IntPoint, Point, RationalPoint, point, rational_point
|
||||||
from .polygon import Polygon
|
from .polygon import Polygon
|
||||||
from .polygon_shape import PolygonShape
|
from .polygon_shape import PolygonShape
|
||||||
from .polyline import Polyline, PolylineShape, segment_box
|
from .polyline import Polyline, PolylineShape, segment_box, shove_segment
|
||||||
from .side import Side, Signum
|
from .side import Side, Signum
|
||||||
from .simplex import Simplex
|
from .simplex import Simplex
|
||||||
from .tile import TileShape
|
from .tile import TileShape
|
||||||
@ -55,4 +55,5 @@ __all__ = [
|
|||||||
"Polyline",
|
"Polyline",
|
||||||
"PolylineShape",
|
"PolylineShape",
|
||||||
"segment_box",
|
"segment_box",
|
||||||
|
"shove_segment",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -53,6 +53,51 @@ def corner_box(corner: IntPoint, half_width: int) -> IntBox:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def shove_segment(corners: list[IntPoint], k: int, dx: int, dy: int) -> list[IntPoint]:
|
||||||
|
"""Displace segment ``k`` (``corners[k]``-``corners[k+1]``) by ``(dx, dy)``.
|
||||||
|
|
||||||
|
``(dx, dy)`` must be perpendicular to the segment (parallel to its adjacent
|
||||||
|
segments), so the trace stays orthogonal. The two endpoints ``corners[0]``
|
||||||
|
and ``corners[-1]`` (pad anchors) are kept fixed: if the moved segment is the
|
||||||
|
first or last, a jog corner is inserted so the trace still starts/ends on its
|
||||||
|
pad; interior neighbours simply resize. Collinear/duplicate corners are
|
||||||
|
dropped. This is the geometric shove primitive.
|
||||||
|
"""
|
||||||
|
n = len(corners)
|
||||||
|
if not (0 <= k < n - 1):
|
||||||
|
return list(corners)
|
||||||
|
new_a = IntPoint(corners[k].x + dx, corners[k].y + dy)
|
||||||
|
new_b = IntPoint(corners[k + 1].x + dx, corners[k + 1].y + dy)
|
||||||
|
result: list[IntPoint] = []
|
||||||
|
if k == 0:
|
||||||
|
result.append(corners[0]) # keep the start pad; jog to the moved run
|
||||||
|
result.append(new_a)
|
||||||
|
else:
|
||||||
|
result.extend(corners[:k]) # neighbour segment resizes to reach new_a
|
||||||
|
result.append(new_a)
|
||||||
|
if k + 1 == n - 1:
|
||||||
|
result.append(new_b)
|
||||||
|
result.append(corners[-1]) # jog back to the end pad
|
||||||
|
else:
|
||||||
|
result.append(new_b)
|
||||||
|
result.extend(corners[k + 2 :])
|
||||||
|
return _dedupe_collinear(result)
|
||||||
|
|
||||||
|
|
||||||
|
def _dedupe_collinear(corners: list[IntPoint]) -> list[IntPoint]:
|
||||||
|
out: list[IntPoint] = []
|
||||||
|
for p in corners:
|
||||||
|
if out and out[-1].x == p.x and out[-1].y == p.y:
|
||||||
|
continue
|
||||||
|
if len(out) >= 2:
|
||||||
|
a, b = out[-2], out[-1]
|
||||||
|
if (b.x - a.x) * (p.y - a.y) - (b.y - a.y) * (p.x - a.x) == 0:
|
||||||
|
out[-1] = p
|
||||||
|
continue
|
||||||
|
out.append(p)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class Polyline:
|
class Polyline:
|
||||||
"""A trace centreline: a sequence of integer corners."""
|
"""A trace centreline: a sequence of integer corners."""
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,14 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from freeroute.geometry import IntBox, IntPoint, Polyline, PolylineShape, segment_box
|
from freeroute.geometry import (
|
||||||
|
IntBox,
|
||||||
|
IntPoint,
|
||||||
|
Polyline,
|
||||||
|
PolylineShape,
|
||||||
|
segment_box,
|
||||||
|
shove_segment,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_polyline_segments():
|
def test_polyline_segments():
|
||||||
@ -42,3 +49,33 @@ def test_polyline_shape_bounding_box():
|
|||||||
def test_diagonal_segment_box_is_conservative_bbox():
|
def test_diagonal_segment_box_is_conservative_bbox():
|
||||||
box = segment_box(IntPoint(0, 0), IntPoint(50, 50), 10)
|
box = segment_box(IntPoint(0, 0), IntPoint(50, 50), 10)
|
||||||
assert box == IntBox(-10, -10, 60, 60)
|
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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user