Add TileShape and Simplex convex-shape core

Ports geometry/planar/TileShape.java (the border-line-based containment,
area, and centre-of-gravity logic) and Simplex.java (a convex region as
the intersection of directed half-planes). Corners are exact
intersections of consecutive border lines; point containment uses exact
side_of. The remove_redundant_lines normalization — dropping lines that
do not contribute and detecting emptiness — is ported line-for-line.

Supporting additions: Line.compare_to/__lt__ (angular sort order),
Line.fast_equals, Line.side_of_intersection, Line.translate (perpendicular
offset), IntDirection.determinant, and IntBox.to_simplex.

offset is approximate (rounded translated lines, as upstream); enlarge
clips to the enlarged bounding box pending the IntOctagon port.

Since there is no JVM oracle, tests assert invariants: corners lie
exactly on their border lines (exact side_of == 0), IntBox -> Simplex
preserves the region over a sampled grid, intersection is contained in
both operands and a point is in the result iff in both, and get_instance
normalization drops redundant lines and detects empty half-plane pairs.
This commit is contained in:
2026-07-11 18:36:20 -06:00
parent d4aa4c729d
commit 1599d181b5
7 changed files with 790 additions and 2 deletions
+82
View File
@@ -113,6 +113,16 @@ class Line:
is_equal_or_opposite = overlaps
def fast_equals(self, other: Line) -> bool:
"""Same line and same direction; fast path for integer lines."""
dx1 = other.a.x - self.a.x
dy1 = other.a.y - self.a.y
dx2 = self.b.x - self.a.x
dy2 = self.b.y - self.a.y
if dx1 * dy2 - dx2 * dy1 != 0:
return False
return self.direction().equals(other.direction())
# --- translation --------------------------------------------------------
def translate_by(self, vector: Vector) -> Line:
@@ -120,6 +130,74 @@ class Line:
return self
return Line(self.a.translate_by(vector), self.b.translate_by(vector))
def translate(self, dist: float) -> Line:
"""Translate the line perpendicular by ``dist`` (left if positive).
Ports ``Line.translate`` — approximate (the offset endpoint is rounded to
integer coordinates), used by ``Simplex.offset``.
"""
v = self.direction().get_vector()
vxvx = v.x * v.x
vyvy = v.y * v.y
length = math.sqrt(vxvx + vyvy)
if vxvx <= vyvy:
rel_x = _round_half_up(dist * length / v.y)
new_a = IntPoint(self.a.x - rel_x, self.a.y)
else:
rel_y = _round_half_up(dist * length / v.x)
new_a = IntPoint(self.a.x, self.a.y + rel_y)
return Line.from_point_direction(new_a, self.direction())
# --- ordering (by direction angle) -------------------------------------
def compare_to(self, other: Line) -> int:
"""Order lines by the angle of their direction with the +x axis (exact).
Ports ``Line.compareTo`` — the ordering ``Simplex`` relies on to sort its
half-plane border lines counter-clockwise.
"""
dx1 = self.b.x - self.a.x
dy1 = self.b.y - self.a.y
dx2 = other.b.x - other.a.x
dy2 = other.b.y - other.a.y
if dy1 > 0:
if dy2 < 0:
return -1
if dy2 == 0:
return 1 if dx2 > 0 else -1
elif dy1 < 0:
if dy2 >= 0:
return 1
else: # dy1 == 0
if dx1 > 0:
if dy2 != 0 or dx2 < 0:
return -1
return 0
if dy2 > 0 or (dy2 == 0 and dx2 > 0):
return 1
if dy2 < 0:
return -1
return 0
determinant = dx2 * dy1 - dy2 * dx1
if determinant > 0:
return 1
if determinant < 0:
return -1
return 0
def __lt__(self, other: Line) -> bool:
return self.compare_to(other) < 0
def side_of_intersection(self, line_1: Line, line_2: Line) -> Side:
"""Side of this line relative to the intersection of ``line_1`` and
``line_2`` (exact fallback after a fast float test). Ports
``Line.side_of_intersection``."""
approx = line_1.intersection_approx(line_2)
result = self.side_of_float(approx, 1.0)
if result == Side.COLLINEAR:
result = self.side_of(line_1.intersection(line_2))
return result
# --- intersection (exact) ----------------------------------------------
def intersection(self, other: Line) -> Point:
@@ -166,6 +244,10 @@ class Line:
)
def _round_half_up(value: float) -> int:
return math.floor(value + 0.5)
def _fast_intersection(
a: IntPoint, oa: IntPoint, delta_1: IntVector, delta_2: IntVector
) -> Point | None: