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:
2026-07-13 02:43:09 -06:00
parent 0bee385e71
commit dffa54a43a
3 changed files with 85 additions and 2 deletions
+2 -1
View File
@@ -24,7 +24,7 @@ from .line import Line
from .point import IntPoint, Point, RationalPoint, point, rational_point
from .polygon import Polygon
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 .simplex import Simplex
from .tile import TileShape
@@ -55,4 +55,5 @@ __all__ = [
"Polyline",
"PolylineShape",
"segment_box",
"shove_segment",
]
+45
View File
@@ -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:
"""A trace centreline: a sequence of integer corners."""