"""``Line`` — a directed line through two integer points. Ports ``geometry/planar/Line.java`` (the subset needed for the router's half-plane work). Like the upstream class, a ``Line`` is defined by two :class:`~freeroute.geometry.point.IntPoint` endpoints. **Arithmetic model: exact.** :meth:`side_of` uses an exact integer determinant, and :meth:`intersection` returns an :class:`~freeroute.geometry.point.IntPoint` when the crossing is integral and a :class:`~freeroute.geometry.point.RationalPoint` otherwise (``z == 0`` when the lines are parallel — the "point at infinity"). :meth:`intersection_approx` is the fast ``float`` path. """ from __future__ import annotations import math from .direction import Direction from .float_point import FloatPoint from .point import IntPoint, Point, rational_point from .side import Side from .vector import IntVector, Vector __all__ = ["Line"] class Line: """A directed line ``a -> b`` with integer endpoints.""" __slots__ = ("a", "b", "_dir") def __init__(self, a: Point, b: Point) -> None: self.a = a self.b = b self._dir: Direction | None = None @staticmethod def from_coords(ax: int, ay: int, bx: int, by: int) -> Line: return Line(IntPoint(ax, ay), IntPoint(bx, by)) @staticmethod def from_point_direction(a: Point, direction: Direction) -> Line: return Line(a, a.translate_by(direction.get_vector())) def __eq__(self, other: object) -> bool: if not isinstance(other, Line): return NotImplemented if self.side_of(other.a) != Side.COLLINEAR: return False return self.direction().equals(other.direction()) def __hash__(self) -> int: return hash((self.a, self.b)) def __repr__(self) -> str: return f"Line({self.a!r}, {self.b!r})" def direction(self) -> Direction: if self._dir is None: self._dir = Direction.get_instance(self.b.difference_by(self.a)) return self._dir def opposite(self) -> Line: return Line(self.b, self.a) # --- orientation -------------------------------------------------------- def side_of(self, point: Point) -> Side: """Side of this line relative to ``point`` (exact). ``ON_THE_LEFT`` if the line passes to the left of the point, ``COLLINEAR`` if the point lies on the line. """ return point.side_of(self.a, self.b).negate() def side_of_float(self, point: FloatPoint, tolerance: float = 0.0) -> Side: """Approximate side test for a :class:`FloatPoint` with a tolerance band.""" ax, ay = self.a.x, self.a.y det = (self.b.y - ay) * (point.x - ax) - (self.b.x - ax) * (point.y - ay) if det - tolerance > 0: return Side.ON_THE_LEFT if det + tolerance < 0: return Side.ON_THE_RIGHT return Side.COLLINEAR def signed_distance(self, point: FloatPoint) -> float: ax, ay = self.a.x, self.a.y dx = self.b.x - ax dy = self.b.y - ay det = dy * (point.x - ax) - dx * (point.y - ay) return det / math.sqrt(dx * dx + dy * dy) def is_orthogonal(self) -> bool: return self.direction().is_orthogonal() def is_diagonal(self) -> bool: return self.direction().is_diagonal() def is_multiple_of_45_degree(self) -> bool: return self.direction().is_multiple_of_45_degree() def is_parallel(self, other: Line) -> bool: return self.direction().side_of(other.direction()) == Side.COLLINEAR def is_perpendicular(self, other: Line) -> bool: v1 = self.direction().get_vector() v2 = other.direction().get_vector() return v1.projection(v2).value == 0 def overlaps(self, other: Line) -> bool: return self.side_of(other.a) == Side.COLLINEAR and self.side_of(other.b) == Side.COLLINEAR 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: if vector.is_zero(): 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 def function_value_approx(self, x: float) -> float: """Approximate ``y`` on this (non-vertical) line at abscissa ``x``.""" p1 = self.a.to_float() p2 = self.b.to_float() dx = p2.x - p1.x if dx == 0: return 0.0 dy = p2.y - p1.y det = p1.x * p2.y - p2.x * p1.y return (dy * x - det) / dx def function_in_y_value_approx(self, y: float) -> float: """Approximate ``x`` on this (non-horizontal) line at ordinate ``y``.""" p1 = self.a.to_float() p2 = self.b.to_float() dy = p2.y - p1.y if dy == 0: return 0.0 dx = p2.x - p1.x det = p1.x * p2.y - p2.x * p1.y return (dx * y + det) / dy # --- intersection (exact) ---------------------------------------------- def intersection(self, other: Line) -> Point: """Exact intersection point; ``result.is_infinite()`` iff parallel.""" a = self.a b = self.b oa = other.a ob = other.b delta_1 = b.difference_by(a) delta_2 = ob.difference_by(oa) if not isinstance(delta_1, IntVector) or not isinstance(delta_2, IntVector): raise NotImplementedError("Line.intersection only supports integer lines") fast = _fast_intersection(a, oa, delta_1, delta_2) if fast is not None: return fast det_1 = a.determinant(b) det_2 = oa.determinant(ob) det = delta_2.determinant(delta_1) is_x = det_1 * delta_2.x - det_2 * delta_1.x is_y = det_1 * delta_2.y - det_2 * delta_1.y if det == 0: return rational_point(is_x, is_y, 0) return rational_point(is_x, is_y, det) def intersection_approx(self, other: Line) -> FloatPoint: """Fast ``float`` intersection; parallel lines give a huge coordinate.""" a, b = self.a, self.b oa, ob = other.a, other.b d1x = b.x - a.x d1y = b.y - a.y d2x = ob.x - oa.x d2y = ob.y - oa.y det_1 = a.x * b.y - a.y * b.x det_2 = oa.x * ob.y - oa.y * ob.x det = d2x * d1y - d2y * d1x if det == 0: big = float(2**31 - 1) return FloatPoint(big, big) return FloatPoint( (d2x * det_1 - d1x * det_2) / det, (d2y * det_1 - d1y * det_2) / det, ) 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: """Closed-form intersection for the orthogonal/45-degree combinations.""" if delta_1.x == 0: # this line vertical if delta_2.y == 0: # other horizontal return IntPoint(a.x, oa.y) if delta_2.x == delta_2.y: # other right diagonal return IntPoint(a.x, oa.y + a.x - oa.x) if delta_2.x == -delta_2.y: # other left diagonal return IntPoint(a.x, oa.y + oa.x - a.x) elif delta_1.y == 0: # this line horizontal if delta_2.x == 0: # other vertical return IntPoint(oa.x, a.y) if delta_2.x == delta_2.y: # other right diagonal return IntPoint(oa.x + a.y - oa.y, a.y) if delta_2.x == -delta_2.y: # other left diagonal return IntPoint(oa.x + oa.y - a.y, a.y) elif delta_1.x == delta_1.y: # this right diagonal if delta_2.x == 0: # other vertical return IntPoint(oa.x, a.y + oa.x - a.x) if delta_2.y == 0: # other horizontal return IntPoint(a.x + oa.y - a.y, oa.y) elif delta_1.x == -delta_1.y: # this left diagonal if delta_2.x == 0: # other vertical return IntPoint(oa.x, a.y + a.x - oa.x) if delta_2.y == 0: # other horizontal return IntPoint(a.x + a.y - oa.y, oa.y) return None