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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user