diff --git a/docs/_static/custom.css b/docs/_static/custom.css new file mode 100644 index 0000000..d0be49f --- /dev/null +++ b/docs/_static/custom.css @@ -0,0 +1,6 @@ +/* Rounded borders on the plots/figures rendered in the page content. + Scoped to the main article so the sidebar logo is left untouched. */ +.content img { + border-radius: 12px; + border: 1px solid var(--color-background-border); +} diff --git a/docs/conf.py b/docs/conf.py index 31548dd..0e629ae 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -81,6 +81,7 @@ html_theme = "furo" html_title = "compute-geometry" html_static_path = ["_static"] +html_css_files = ["custom.css"] html_logo = "../public/logo.svg" html_favicon = "../public/logo.svg" diff --git a/docs/examples.md b/docs/examples.md new file mode 100644 index 0000000..3d30e57 --- /dev/null +++ b/docs/examples.md @@ -0,0 +1,172 @@ +# Examples + +One complete, self-contained example for each algorithm in the library. Every +snippet runs as-is: it builds the input, runs the algorithm, inspects the +result, and draws it with the matching visualization helper. The `# ->` comments +show the actual output, and each example is followed by the figure it produces. + +## Convex hull + +The smallest convex polygon enclosing a set of points (Gift Wrapping / Jarvis +march). + +```python +from cgeom.algorithms import ConvexHull +from cgeom.visualization import plot_convex_hull + +points = [(326, 237), (373, 209), (378, 265), (443, 241), + (396, 231), (416, 270), (361, 335), (324, 297)] + +hull = ConvexHull(points) + +hull.convex_hull() # ordered hull vertices, [[x, y], ...] +hull.get_indexes() # -> [1, 3, 6, 7, 0] (indices into `points`) + +plot_convex_hull(hull) +``` + +```{image} ../public/examples/convex_hull.png +:alt: Convex hull of a point set +:align: center +:width: 70% +``` + +## Minimum enclosing circle + +The smallest circle that contains every point. + +```python +import numpy as np +from cgeom import Circle +from cgeom.algorithms import MinimumCircle +from cgeom.visualization import plot_min_circle + +points = [(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)] + +mc = MinimumCircle() +center, radius = mc.minimum_circle(points) # -> [0.5, 0.5], 0.70710678... + +# the raw [[cx, cy], radius] result drops straight into a Circle primitive +circle = Circle(mc.minimum_circle(points)) +circle.area # 1.5707... + +plot_min_circle(mc, np.array(points)) +``` + +```{image} ../public/examples/min_circle.png +:alt: Exact and heuristic minimum enclosing circle +:align: center +:width: 100% +``` + +## Delaunay triangulation + +A triangulation that maximizes the minimum angle — no point lies inside any +triangle's circumcircle. + +```python +from cgeom.algorithms import DelaunayTriangulation +from cgeom.visualization import plot_delaunay + +points = [(0, 0), (4, 0), (4, 4), (0, 4), (2, 2)] + +dt = DelaunayTriangulation(points) + +dt.triangulate() # -> [[0, 1, 4], [0, 3, 4], [1, 2, 4], [2, 3, 4]] +len(dt.get_edges()) # -> 8 +len(dt.get_circumcircles()) # -> 4 + +plot_delaunay(dt, show_circumcircles=True) +``` + +```{image} ../public/examples/delaunay.png +:alt: Delaunay triangulation with circumcircles +:align: center +:width: 70% +``` + +## Voronoi diagram + +Partitions the plane into cells, one per site, containing everything closest to +that site — the dual of the Delaunay triangulation. + +```python +import numpy as np +from cgeom.algorithms import VoronoiDiagram +from cgeom.visualization import plot_voronoi + +points = np.loadtxt("examples/points1.txt") # 27 sites + +voronoi = VoronoiDiagram(points) +cells = voronoi.build_voronoi_diagram() # one cell per site +len(cells) # -> 27 + +plot_voronoi(voronoi, cells) +``` + +```{image} ../public/examples/voronoi.png +:alt: Voronoi diagram of 27 sites +:align: center +:width: 70% +``` + +## Polygon triangulation + +Decomposes a simple polygon into triangles by ear clipping. The polygon is +triangulated on construction; the diagonals are available immediately. + +```python +from cgeom.algorithms import PolygonTriangulation +from cgeom.visualization import plot_triangulation + +# a non-convex pentagon, vertices counter-clockwise +poly = [[0, 0], [4, 0], [4, 4], [2, 2], [0, 4]] + +pt = PolygonTriangulation(poly) + +pt.diagonals # the added diagonals, [[[x1, y1], [x2, y2]], ...] +pt.get_diag_vertexes() # -> [[4, 1], [1, 3]] (vertex-index pairs) + +plot_triangulation(pt) +``` + +```{image} ../public/examples/triangulation.png +:alt: Ear-clipping triangulation of a non-convex polygon +:align: center +:width: 70% +``` + +## Segment intersection + +Reports every pairwise crossing of a set of segments using the Bentley–Ottmann +sweep line, with a brute-force method for verification. + +```python +from cgeom.algorithms import SegmentIntersection +from cgeom.visualization import plot_intersections + +segments = [ + [[0, 0], [4, 4]], # diagonal / + [[0, 4], [4, 0]], # diagonal \ + [[0, 2], [4, 2]], # horizontal + [[2, 0], [2, 4]], # vertical +] + +si = SegmentIntersection(segments) + +si.find_intersections() # sweep line -> [[2.0, 2.0]] +si.find_intersections_brute_force() # cross-check -> [[2.0, 2.0]] +si.get_intersection_pairs() # (i, j, point) for each crossing pair + +plot_intersections(si) +``` + +```{image} ../public/examples/intersection.png +:alt: Segment intersections found by the sweep line +:align: center +:width: 70% +``` + +See the [User guide](guide/algorithms) for the full method-by-method reference, +or the [API reference](api/index) for complete signatures. The figures above are +regenerated by `docs/generate_example_figures.py`. diff --git a/docs/generate_example_figures.py b/docs/generate_example_figures.py new file mode 100644 index 0000000..6a7b206 --- /dev/null +++ b/docs/generate_example_figures.py @@ -0,0 +1,104 @@ +"""Regenerate the figures embedded in ``docs/examples.md``. + +Each visualization helper ends in ``plt.show()``; here we patch ``show`` to save +the current figure instead, so the rendered output matches the documented code. + +Run from the repository root:: + + .venv/bin/python docs/generate_example_figures.py +""" + +import os + +import matplotlib + +matplotlib.use("Agg") +import matplotlib.pyplot as plt # noqa: E402 +import numpy as np # noqa: E402 + +from cgeom.algorithms import ( # noqa: E402 + ConvexHull, + DelaunayTriangulation, + MinimumCircle, + PolygonTriangulation, + SegmentIntersection, + VoronoiDiagram, +) +from cgeom.visualization import ( # noqa: E402 + plot_convex_hull, + plot_delaunay, + plot_intersections, + plot_min_circle, + plot_triangulation, + plot_voronoi, +) + +OUT_DIR = "public/examples" +os.makedirs(OUT_DIR, exist_ok=True) + +_saved = set() +_current = {"name": None} + + +def _save_show(): + """Stand-in for ``plt.show`` that saves the first figure per example.""" + name = _current["name"] + if name and name not in _saved: + fig = plt.gcf() + fig.savefig( + os.path.join(OUT_DIR, f"{name}.png"), + dpi=150, + facecolor=fig.get_facecolor(), + bbox_inches="tight", + ) + _saved.add(name) + plt.close("all") + + +plt.show = _save_show + + +def render(name, fn): + _current["name"] = name + fn() + print(f"saved {OUT_DIR}/{name}.png") + + +# --- Convex hull ----------------------------------------------------------- +hull = ConvexHull( + [ + (326, 237), + (373, 209), + (378, 265), + (443, 241), + (396, 231), + (416, 270), + (361, 335), + (324, 297), + ] +) +render("convex_hull", lambda: plot_convex_hull(hull)) + +# --- Minimum enclosing circle --------------------------------------------- +mc_points = np.array([(0, 0), (1, 0), (0, 1), (1, 1), (0.5, 0.5)]) +render("min_circle", lambda: plot_min_circle(MinimumCircle(), mc_points, show=True)) + +# --- Delaunay triangulation ------------------------------------------------ +dt = DelaunayTriangulation([(0, 0), (4, 0), (4, 4), (0, 4), (2, 2)]) +dt.triangulate() +render("delaunay", lambda: plot_delaunay(dt, show_circumcircles=True)) + +# --- Voronoi diagram ------------------------------------------------------- +voronoi = VoronoiDiagram(np.loadtxt("examples/points1.txt")) +cells = voronoi.build_voronoi_diagram() +render("voronoi", lambda: plot_voronoi(voronoi, cells)) + +# --- Polygon triangulation ------------------------------------------------- +pt = PolygonTriangulation([[0, 0], [4, 0], [4, 4], [2, 2], [0, 4]]) +render("triangulation", lambda: plot_triangulation(pt)) + +# --- Segment intersection -------------------------------------------------- +si = SegmentIntersection( + [[[0, 0], [4, 4]], [[0, 4], [4, 0]], [[0, 2], [4, 2]], [[2, 0], [2, 4]]] +) +render("intersection", lambda: plot_intersections(si)) diff --git a/docs/guide/algorithms.md b/docs/guide/algorithms.md index 026cd82..bd7fe2e 100644 --- a/docs/guide/algorithms.md +++ b/docs/guide/algorithms.md @@ -85,11 +85,14 @@ si.get_intersection_pairs() # (i, j, point) tuples ## PolygonTriangulation -Triangulates a simple polygon. +Triangulates a simple polygon by ear clipping. The polygon is triangulated on +construction, so the diagonals are available immediately — there is no separate +`triangulate()` call. ```python -pt = PolygonTriangulation([[0, 0], [4, 0], [4, 4], [0, 4]]) -pt.triangulate() +pt = PolygonTriangulation([[0, 0], [4, 0], [4, 4], [2, 2], [0, 4]]) +pt.diagonals # the diagonals added, [[[x1, y1], [x2, y2]], ...] +pt.get_diag_vertexes() # the same diagonals as vertex-index pairs ``` ## Input validation diff --git a/docs/index.md b/docs/index.md index 74f443e..68c9ca5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,13 +1,50 @@ # compute-geometry -A research-focused computational geometry library for Python, providing a set of -composable primitives and algorithms for solving geometric problems. +**compute-geometry** is a research-focused computational geometry library for +Python. It pairs a small set of validated geometric primitives with classical +algorithms and a cohesive visualization layer, so you can move from raw +coordinates to a publication-ready figure in just a few lines. -`compute-geometry` ships geometric **primitives** (points, segments, lines, circles -and polygons) validated with Pydantic, a collection of classical **algorithms** -(convex hull, Voronoi diagrams, Delaunay and polygon triangulation, minimum -enclosing circle and segment intersection), and a polished **visualization** layer -built on Matplotlib. +The library is intentionally compact and composable. Primitives validate their +own inputs, algorithms accept plain points or segments, and every algorithm has +a matching one-call plotting helper — the three layers are designed to be used +together or independently. + +```{image} ../public/elements.png +:alt: The geometric primitives of compute-geometry +:align: center +``` + +## Installation + +`compute-geometry` requires **Python 3.12+** and is imported as `cgeom`: + +```bash +uv add compute-geometry # or: pip install compute-geometry +``` + +See the [installation guide](installation) for building from source and for the +documentation toolchain. + +## The three layers + +**Primitives.** `Point`, `Line`, `Segment`, `Circle`, and `Polygon` are +immutable, Pydantic-validated models. They accept flexible construction forms — +keyword arguments, lists, tuples, or NumPy arrays — and reject malformed input +at construction time, so downstream code never sees an invalid shape. + +**Algorithms.** The `cgeom.algorithms` module bundles the classics: convex hull, +minimum enclosing circle, Delaunay triangulation, Voronoi diagrams, polygon +triangulation, and segment intersection. Each is a small class that takes a set +of points or segments and exposes its result through clearly named methods. + +**Visualization.** The `cgeom.visualization` module provides one Matplotlib +helper per algorithm. They share a clean navy-and-blue palette and sensible +defaults, so a single call turns an algorithm's output into a finished figure. + +## A first look + +Build a Voronoi diagram from a set of sites and plot it: ```python import numpy as np @@ -21,20 +58,37 @@ cells = voronoi.build_voronoi_diagram() plot_voronoi(voronoi, cells) ``` +## Where to next + +- **[Quickstart](quickstart)** — the core workflow in one page: build, compute, plot. +- **[Examples](examples)** — a complete, runnable example and figure for every algorithm. +- **[User guide](guide/elements)** — primitives, algorithms, and visualization in depth. +- **[API reference](api/index)** — full signatures generated from the docstrings. + ```{toctree} :maxdepth: 2 :caption: Getting started +:hidden: installation quickstart +examples ``` ```{toctree} :maxdepth: 2 :caption: User guide +:hidden: guide/elements guide/algorithms guide/visualization ``` +```{toctree} +:maxdepth: 2 +:caption: Reference +:hidden: + +api/index +``` diff --git a/public/examples/convex_hull.png b/public/examples/convex_hull.png new file mode 100644 index 0000000..b17859c Binary files /dev/null and b/public/examples/convex_hull.png differ diff --git a/public/examples/delaunay.png b/public/examples/delaunay.png new file mode 100644 index 0000000..3b5f00f Binary files /dev/null and b/public/examples/delaunay.png differ diff --git a/public/examples/intersection.png b/public/examples/intersection.png new file mode 100644 index 0000000..7e8efba Binary files /dev/null and b/public/examples/intersection.png differ diff --git a/public/examples/min_circle.png b/public/examples/min_circle.png new file mode 100644 index 0000000..b6dec40 Binary files /dev/null and b/public/examples/min_circle.png differ diff --git a/public/examples/triangulation.png b/public/examples/triangulation.png new file mode 100644 index 0000000..7fe377c Binary files /dev/null and b/public/examples/triangulation.png differ diff --git a/public/examples/voronoi.png b/public/examples/voronoi.png new file mode 100644 index 0000000..3892c2a Binary files /dev/null and b/public/examples/voronoi.png differ