| """Demo module expanded for safe demo.
|
|
|
| This module provides a compact geometry/vector utility library used by the
|
| demo test. It's self-contained, pure Python, and intended to be safe to add
|
| to the repository without changing any project-wide dependencies.
|
|
|
| The file intentionally contains a variety of helper functions and small
|
| classes to simulate a larger, realistic module while remaining easy to
|
| understand and test.
|
| """
|
|
|
| from __future__ import annotations
|
|
|
| import math
|
| import random
|
| from typing import Iterable, List, Tuple
|
|
|
|
|
| class Vector:
|
| """A small immutable 2D vector class with utility methods."""
|
|
|
| __slots__ = ("x", "y")
|
|
|
| def __init__(self, x: float, y: float) -> None:
|
| self.x = float(x)
|
| self.y = float(y)
|
|
|
| def __iter__(self):
|
| yield self.x
|
| yield self.y
|
|
|
| def __repr__(self) -> str:
|
| return f"Vector({self.x:.4f}, {self.y:.4f})"
|
|
|
| def __add__(self, other: "Vector") -> "Vector":
|
| return Vector(self.x + other.x, self.y + other.y)
|
|
|
| def __sub__(self, other: "Vector") -> "Vector":
|
| return Vector(self.x - other.x, self.y - other.y)
|
|
|
| def __mul__(self, scalar: float) -> "Vector":
|
| return Vector(self.x * scalar, self.y * scalar)
|
|
|
| def dot(self, other: "Vector") -> float:
|
| return self.x * other.x + self.y * other.y
|
|
|
| def cross(self, other: "Vector") -> float:
|
| """2D cross product (scalar) used for orientation tests."""
|
| return self.x * other.y - self.y * other.x
|
|
|
| def length(self) -> float:
|
| return math.hypot(self.x, self.y)
|
|
|
| def normalized(self) -> "Vector":
|
| l = self.length()
|
| if l == 0:
|
| return Vector(0.0, 0.0)
|
| return Vector(self.x / l, self.y / l)
|
|
|
| def rotate(self, angle_rad: float) -> "Vector":
|
| """Rotate vector counter-clockwise by angle in radians."""
|
| c = math.cos(angle_rad)
|
| s = math.sin(angle_rad)
|
| return Vector(self.x * c - self.y * s, self.x * s + self.y * c)
|
|
|
|
|
| def polygon_area(points: Iterable[Vector]) -> float:
|
| """Compute polygon area using the shoelace formula.
|
|
|
| Works for convex and non-convex polygons (signed area).
|
| """
|
| pts = list(points)
|
| if len(pts) < 3:
|
| return 0.0
|
| area = 0.0
|
| for i in range(len(pts)):
|
| a = pts[i]
|
| b = pts[(i + 1) % len(pts)]
|
| area += a.x * b.y - a.y * b.x
|
| return 0.5 * abs(area)
|
|
|
|
|
| def centroid(points: Iterable[Vector]) -> Vector:
|
| pts = list(points)
|
| if not pts:
|
| return Vector(0.0, 0.0)
|
| sx = sum(p.x for p in pts)
|
| sy = sum(p.y for p in pts)
|
| n = len(pts)
|
| return Vector(sx / n, sy / n)
|
|
|
|
|
| def is_convex_polygon(points: Iterable[Vector]) -> bool:
|
| pts = list(points)
|
| if len(pts) < 4:
|
| return True
|
| sign = 0
|
| n = len(pts)
|
| for i in range(n):
|
| a = pts[i]
|
| b = pts[(i + 1) % n]
|
| c = pts[(i + 2) % n]
|
| cross = (b - a).cross(c - b)
|
| if cross != 0:
|
| s = 1 if cross > 0 else -1
|
| if sign == 0:
|
| sign = s
|
| elif sign != s:
|
| return False
|
| return True
|
|
|
|
|
| def bounding_box(points: Iterable[Vector]) -> Tuple[Vector, Vector]:
|
| pts = list(points)
|
| if not pts:
|
| return Vector(0.0, 0.0), Vector(0.0, 0.0)
|
| minx = min(p.x for p in pts)
|
| miny = min(p.y for p in pts)
|
| maxx = max(p.x for p in pts)
|
| maxy = max(p.y for p in pts)
|
| return Vector(minx, miny), Vector(maxx, maxy)
|
|
|
|
|
| def generate_random_polygon(n: int, radius: float = 1.0, seed: int | None = None) -> List[Vector]:
|
| """Generate a simple random star-like polygon (not guaranteed simple).
|
|
|
| This is intentionally simple for the demo and is not intended as a
|
| production polygon generator.
|
| """
|
| if seed is not None:
|
| random.seed(seed)
|
| angles = sorted(random.random() * 2 * math.pi for _ in range(n))
|
| pts = [Vector(math.cos(a) * radius * (0.5 + random.random() / 2),
|
| math.sin(a) * radius * (0.5 + random.random() / 2))
|
| for a in angles]
|
| return pts
|
|
|
|
|
|
|
|
|
| def make_vector(x: float, y: float) -> Vector:
|
| return Vector(x, y)
|
|
|
|
|
| def rotate_polygon(points: Iterable[Vector], angle_rad: float) -> List[Vector]:
|
| return [p.rotate(angle_rad) for p in points]
|
|
|
|
|
| def translate_polygon(points: Iterable[Vector], offset: Vector) -> List[Vector]:
|
| return [p + offset for p in points]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| def _make_op(i: int):
|
| def op(x: float, y: float) -> float:
|
|
|
| a = x + (i % 5) * 0.1
|
| b = y - (i % 7) * 0.07
|
| return (a * a + b * b) ** 0.5 + (i % 3) * 0.001
|
|
|
| op.__name__ = f"op_{i}"
|
| return op
|
|
|
|
|
|
|
| _OPS = {f"op_{i}": _make_op(i) for i in range(1, 201)}
|
|
|
|
|
| def apply_ops(x: float, y: float) -> List[float]:
|
| """Apply all small ops to the given inputs and return results.
|
|
|
| Used by tests to verify the demo module behaves deterministically.
|
| """
|
| return [fn(x, y) for fn in _OPS.values()]
|
|
|
|
|
| def demo_example(seed: int = 42) -> dict:
|
| """Return a deterministic demo payload used by tests and CLI.
|
|
|
| The payload includes a generated polygon, its area and centroid, and
|
| a short list of op results to verify deterministic behavior.
|
| """
|
| poly = generate_random_polygon(6, radius=1.5, seed=seed)
|
| area = polygon_area(poly)
|
| cent = centroid(poly)
|
| ops = apply_ops(0.5, -0.25)[:5]
|
| return {
|
| "area": area,
|
| "centroid": (cent.x, cent.y),
|
| "ops": ops,
|
| }
|
|
|
|
|
| def demo_run_cli() -> None:
|
| """Simple CLI runner used when demo.py is executed as a script."""
|
| payload = demo_example()
|
| print("Demo payload:")
|
| print(f" area={payload['area']:.6f}")
|
| print(f" centroid={payload['centroid']}")
|
| print(" ops:")
|
| for v in payload["ops"]:
|
| print(f" - {v:.6f}")
|
|
|
|
|
| if __name__ == "__main__":
|
| poly = generate_random_polygon(8, radius=2.0, seed=42)
|
| print("Polygon area:", polygon_area(poly))
|
| print("Centroid:", centroid(poly))
|
| print("Is convex:", is_convex_polygon(poly))
|
|
|