GeomPP 0.8.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package GeomPP --version 0.8.0
                    
NuGet\Install-Package GeomPP -Version 0.8.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="GeomPP" Version="0.8.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="GeomPP" Version="0.8.0" />
                    
Directory.Packages.props
<PackageReference Include="GeomPP" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add GeomPP --version 0.8.0
                    
#r "nuget: GeomPP, 0.8.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package GeomPP@0.8.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=GeomPP&version=0.8.0
                    
Install as a Cake Addin
#tool nuget:?package=GeomPP&version=0.8.0
                    
Install as a Cake Tool

← back

GeomPP — C# Bindings

C++/CLI bindings for geompp, targeting Windows x64.

Available on NuGet as GeomPP.

Supports .NET 8 (and later) and .NET Framework 4.8.

Changelog — full release notes for every version.


Install

dotnet add package GeomPP

or in your .csproj:

<PackageReference Include="GeomPP" Version="0.8.0" />

Quick start

Example 1 — LineSegment3D intersection

The code:

using G = GeomPP;

G.Precision.DecimalPrecision = G.Precision.DP_THREE;

var s1 = G.LineSegment3D.Make(new G.Point3D(1, 0, 0), new G.Point3D(-1, 0, 2));
var s2 = G.LineSegment3D.Make(new G.Point3D(0, 1, 0), new G.Point3D(0, -1, 2));

Console.WriteLine($"s1 = {s1}");
Console.WriteLine($"s2 = {s2}");

var result = s1.Intersection(s2);
if (result is G.Point3D p) {
  Console.WriteLine("OK: " + p.ToWkt());  // expects POINT (0 0 1)
} else {
  Console.WriteLine("FAIL: no intersection");
}

Output:

s1 = LINESTRING (1 0 0, -1 0 2)
s2 = LINESTRING (0 1 0, 0 -1 2)
intersection found: POINT (0 0 1)
intersection written to intersection.wkt

Example 2 — Load geometries from an .lsv file

An .lsv file is a plain-text list of WKT geometries, one per line:

POINT (1 2 3)
POINT (4 5 6)
LINESTRING (0 0 0, 1 1 1)
LINESTRING (2 0 0, 2 3 4)
LINE (0 0 0, 1 0 0)
RAY (0 0 0, 0 1 0)
using G = GeomPP;

G.Precision.DecimalPrecision = G.Precision.DP_THREE;

var parser = G.WktParser.Open("sample_geometries.lsv");

if (!parser.HasNext()) {
    Console.WriteLine("no geometries found");
    return;
}

while (parser.HasNext()) {
    var item = parser.Next();
    if (item == null) {
        Console.WriteLine("skipped unrecognised line");
        continue;
    }
    Console.WriteLine(G.WktParser.ToWkt(item));
}

Output:

POINT (1 2 3)
POINT (4 5 6)
LINESTRING (0 0 0, 1 1 1)
LINESTRING (2 0 0, 2 3 4)
LINE (0 0 0, 1 0 0)
RAY (0 0 0, 0 1 0)

Example 3 — are_coplanar, winding order, and polygon with holes

using GeomPP;
using Geompp.Extensions;

Precision.DecimalPrecision = Precision.DP_THREE;

var flat = new List<Point3D> {
    new Point3D(0,0,0), new Point3D(1,0,0),
    new Point3D(0,1,0), new Point3D(1,1,0)
};
var skew = new List<Point3D> {
    new Point3D(0,0,0), new Point3D(1,0,0),
    new Point3D(0,1,0), new Point3D(0,0,1)
};

Console.WriteLine(flat.AreCoplanar());   // True  — all on the XY plane
Console.WriteLine(skew.AreCoplanar());   // False — spans 3D space

// Closest world-axis plane and winding check
var plane = flat.ClosestWorldPlaneTo();
Console.WriteLine(plane.Normal());       // (0, 0, 1)  → XY plane

Console.WriteLine(flat.AreCCW());        // True  — CCW on the XY plane
Console.WriteLine(flat.AreCW());         // False

// Polygon3D requires CCW outer ring and CW holes
var outer = new List<Point3D> {
    new Point3D(0,0,0), new Point3D(4,0,0),
    new Point3D(4,4,0), new Point3D(0,4,0)
};
var hole = new List<Point3D> {
    new Point3D(1,3,0), new Point3D(3,3,0),
    new Point3D(3,1,0), new Point3D(1,1,0)
};
var poly = Polygon3D.Make(outer, new List<List<Point3D>> { hole });
Console.WriteLine(poly.Size());          // 4

Output:

True
False
VECTOR (0 0 1)
True
False
4

Precision

All floating-point comparisons go through a thread-local precision setting:

G.Precision.DecimalPrecision = G.Precision.DP_THREE;   // 3 decimal places (default)
G.Precision.DecimalPrecision = G.Precision.DP_SIX;     // 6 decimal places
G.Precision.DecimalPrecision = G.Precision.DP_NINE;    // 9 decimal places

double eps = G.Precision.Epsilon;  // current epsilon (10^-N)

Supported types

Type 2D 3D
Point
Vector
Line
Ray
LineSegment
Polyline
Triangle
Polygon
BBox
Plane
GeometryCollection
WktParser

All types expose ToWkt(), FromWkt(), ToFile(), FromFile(), AlmostEquals(), and the same operators available in the C++ library.

Intersection methods return object (null when there is no intersection); use C# pattern matching to extract the result type:

var result = line.Intersection(segment);
if (result is G.Point2D p)      { /* point intersection */ }
if (result is G.LineSegment2D s) { /* overlap */ }

Plane exposes intersections, parallel / coplanar checks, and convenience constructors:

var pl  = G.Plane.XY();
var ray = G.Ray3D.Make(new G.Point3D(5, 3, 4), new G.Vector3D(0, 0, -1));
var hit = pl.Intersection(ray);            // Point3D(5, 3, 0)  or null
var axisY = pl.Intersection(G.Plane.YZ()); // Line3D along the Y-axis  or null

pl.IsParallel(ray);                        // false (ray crosses the plane)
pl.IsCoplanar(G.Line3D.Make(new G.Point3D(0,0,0), new G.Point3D(1,1,0))); // true

// Implicit Vector → Point construction
var pFromV = new G.Point3D(new G.Vector3D(1, 2, 3));  // = Point3D(1, 2, 3)

// Triangle3D intersection with Line / Ray / Segment / Plane / Triangle
var tri    = G.Triangle3D.Make(new G.Point3D(0,0,0), new G.Point3D(4,0,0), new G.Point3D(0,4,0));
var hitL   = tri.Intersection(G.Line3D.Make(new G.Point3D(1, 1, -1), new G.Point3D(1, 1, 1)))             as G.Point3D;
var hitR   = tri.Intersection(G.Ray3D.Make(new G.Point3D(1, 1, 4),  new G.Vector3D(0, 0, -1)))            as G.Point3D;
var hitS   = tri.Intersection(G.LineSegment3D.Make(new G.Point3D(1, 1, -2), new G.Point3D(1, 1, 3)))      as G.Point3D;
var y1     = G.Plane.FromOriginAndNormal(new G.Point3D(0, 1, 0), new G.Vector3D(0, 1, 0));
var hitP   = tri.Intersection(y1)                                                                          as G.LineSegment3D;  // (0,1,0)→(3,1,0)
var other  = G.Triangle3D.Make(new G.Point3D(1,1,-1), new G.Point3D(1,1,1), new G.Point3D(3,1,0));
var hitT   = tri.Intersection(other)                                                                       as G.LineSegment3D;  // (1,1,0)→(3,1,0)

Platform note

GeomPP is built with C++/CLI and is Windows x64 only. It will not run on Linux, macOS, or 32-bit processes.

Build on Windows x64 manually

If you want to built from this source files, use these commands

Build the C# DLL

# from the main directory, geompp

# if you want to build for .Net 8
msbuild geompp_csharp\GeomPP.vcxproj /p:Platform=x64 /p:GeomppBuildRoot="$PWD\build_win" [/p:Configuration=Release]

# if you want to build for .Net Framework 4.8
msbuild geompp_csharp\GeomPP_Net48.vcxproj /p:Platform=x64 /p:GeomppBuildRoot="$PWD\build_win" [/p:Configuration=Release]

# run smoke tests, after build from the main directory geompp
dotnet test geompp_csharp\tests\GeomPPTests.csproj [-p:GeomPPConfiguration=Release]
Product Compatible and additional computed target framework versions.
.NET net10.0-windows7.0 is compatible. 
.NET Framework net48 is compatible.  net481 was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETFramework 4.8

    • No dependencies.
  • net10.0-windows7.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.17.3 98 8/15/2026
0.17.2 92 8/14/2026
0.16.2 87 8/6/2026
0.15.1 119 7/24/2026
0.14.0 104 7/21/2026
0.13.0 117 7/9/2026
0.12.0 103 7/8/2026
0.11.0 110 7/4/2026
0.10.1 124 6/23/2026
0.9.1 222 6/17/2026
0.8.2 117 5/18/2026
0.8.0 111 5/18/2026
0.7.0 104 5/6/2026
0.6.0 104 5/4/2026
0.5.0 103 5/1/2026
0.4.0 115 4/27/2026
0.1.2 124 4/13/2026
0.1.1 123 4/13/2026
0.1.0 125 4/9/2026

> C++ library — tagged `v0.8.0` · C# / NuGet — tagged `csharp-v0.8.0` · Python / PyPI — tagged `python-v0.8.0`

> Touches `Plane`, `Triangle3D`, `Point2D`, `Point3D`, `Line3D`, `Ray3D`, `LineSegment3D`. Adds the `Distance` / `DistanceTo` family across every pair of 3D linear primitives, factors the closest-points-of-two-lines math into a new `calc_utils3d` header, and introduces a Doxygen-driven API documentation pipeline. Closes 6 stub methods (19 → 13).

### Added

**C++ core**
- `Plane::Intersects(Ray3D)` / `Plane::Intersection(Ray3D)` — delegates to the line case, then keeps the hit only if it is ahead of the ray's origin.
- `Plane::Intersects(LineSegment3D)` / `Plane::Intersection(LineSegment3D)` — delegates to the line case, then keeps the hit only if it lies within the segment.
- `Plane::Intersects(Plane)` / `Plane::Intersection(Plane)` — closed-form line of intersection from the cross product of the two normals and a point in the span of those normals; returns a `Line3D` (direction = `N1 × N2`).
- `Plane::Intersects(Triangle3D)` / `Plane::Intersection(Triangle3D)` — delegates to the new `Triangle3D::Intersection(Plane)` (unwraps/rewraps because the two `ReturnSet` variants don't share alternatives).
- `Plane::IsParallel(Line3D / Ray3D / LineSegment3D)` — tests `direction · normal == 0`. Returns `true` for lines/rays/segments lying *in* the plane (coplanar ⊂ parallel by this definition).
- `Plane::IsCoplanar(Line3D / Ray3D / LineSegment3D)` — strict-subset of parallel: requires the first point to also lie on the plane.
- `Triangle3D::Intersection(Line3D)` — intersects the line with the triangle's plane, then runs a 3D barycentric inside-test (`U·U`, `V·V`, `U·V`, `W·U`, `W·V`) using `within_axis_boundary(sc, tc)`. No 2D projection.
- `Triangle3D::Intersection(Ray3D)` — line-intersection result, kept only if `ray.IsAhead(hit)`.
- `Triangle3D::Intersection(LineSegment3D)` — line-intersection result, kept only if `segment.Contains(hit)`.
- `Triangle3D::Intersection(Plane)` — plane-plane intersection line, then projects both the line and the triangle vertices into the triangle's 2D frame and delegates to `Triangle2D::Intersection(Line2D)`; lifts the resulting `Point2D`/`LineSegment2D` back to 3D via `Plane::Evaluate`.
- `Triangle3D::Intersection(Triangle3D)` — computes each triangle's intersection with the other's plane (both yield collinear `LineSegment3D`s on the planes' common line) and returns the segment overlap.
- `Triangle3D::Intersects(Plane)` — `bool` overload.
- `Point2D(Vector2D const&)` — implicit construction from a vector. Mirrors the new `Point3D(Vector3D const&)` constructor.
- `Point3D(Vector3D const&)` — implicit construction from a vector. Lets `Vector3D` arithmetic results flow directly into `Point3D`-typed APIs (e.g., `Line3D::Make(Vector3D-result, …)`).
- `calc_utils3d.hpp` / `.cpp` — new header exposing two free functions:
 - `distance_line_to_line(L1_P0, L1_P1, L2_P0, L2_P1, sc, tc)` — solves the 2×2 perpendicular-distance system between two parameterized lines using Cramer's rule; `sc=0` / `tc=largest-denominator-projection` in the parallel branch (gives the "magic zero" when lines are collinear).
 - `intersection_line_to_line(L1_P0, L1_P1, L2_P0, L2_P1, sc, tc) -> std::optional<Point3D>` — same setup, but returns the unique intersection point when the closest-approach distance is zero and the lines aren't parallel.
- `Line3D::Distance(Line3D|Ray3D|LineSegment3D)` — directed `LineSegment3D` from this line's closest point to the other primitive's closest point; `nullopt` when they intersect or overlap.
- `Line3D::DistanceTo(Line3D|Ray3D|LineSegment3D)` — scalar distance; 0 when they intersect or overlap.
- `Ray3D::Distance(Line3D|Ray3D|LineSegment3D)` / `Ray3D::DistanceTo(...)` — same shape for rays, with the ray-parameter clamp `sc >= 0`.
- `LineSegment3D::Distance(Line3D|Ray3D|LineSegment3D)` / `LineSegment3D::DistanceTo(...)` — same shape for segments, with the segment-parameter clamp `sc ∈ [0, 1]`.
- `LineSegment3D::Flip()` — returns a segment with endpoints swapped.

**Python / PyPI**
- `Plane.intersects(ray|segment|plane|triangle)` / `Plane.intersection(...)` overloads added.
- `Plane.is_parallel(line|ray|segment)` and `Plane.is_coplanar(line|ray|segment)` added.
- `Triangle3D.intersects(plane)` and `Triangle3D.intersection(plane)` overloads added.
- `Triangle3D.intersection(line|ray|segment|triangle)` now return real results (the bindings existed before but routed to stubs that threw).
- `Point2D(Vector2D)` and `Point3D(Vector3D)` constructors exposed via `py::init<const Vector2D&>()` / `py::init<const Vector3D&>()`.
- `Line3D.distance(other|ray|segment)` / `Line3D.distance_to(other|ray|segment)`.
- `Ray3D.distance(line|other|segment)` / `Ray3D.distance_to(line|other|segment)`.
- `LineSegment3D.distance(line|ray|other)` / `LineSegment3D.distance_to(line|ray|other)`.
- `LineSegment3D.flip()`.

**C# / NuGet**
- `Plane.Intersects(Ray3D^|LineSegment3D^|Plane^|Triangle3D^)` and matching `Plane.Intersection(...)` overloads. Plane∩Plane returns a `Line3D^` (or `null`); Plane∩Triangle returns a `Point3D^` / `LineSegment3D^` (or `null`).
- `Plane.IsParallel(Line3D^|Ray3D^|LineSegment3D^)` and `Plane.IsCoplanar(Line3D^|Ray3D^|LineSegment3D^)`.
- `Triangle3D.Intersects(Plane^)` and `Triangle3D.Intersection(Plane^)` overloads.
- `Triangle3D.Intersection(Line3D^|Ray3D^|LineSegment3D^|Triangle3D^)` now return real results (the bindings existed but the native side threw).
- `Point2D(Vector2D^)` and `Point3D(Vector3D^)` constructors.
- `Line3D.Distance(Line3D^|Ray3D^|LineSegment3D^)` / `Line3D.DistanceTo(...)`.
- `Ray3D.Distance(Line3D^|Ray3D^|LineSegment3D^)` / `Ray3D.DistanceTo(...)`.
- `LineSegment3D.Distance(Line3D^|Ray3D^|LineSegment3D^)` / `LineSegment3D.DistanceTo(...)`.

**Tooling / Documentation**
- `docs/Doxyfile` — Doxygen config: reads `geompp/include/*.hpp`, emits HTML reference under `docs/api/cpp/html/` and machine-readable XML under `docs/api/cpp/xml/`.
- `docs/gen_bindings_md.py` — transforms the Doxygen XML into per-class Markdown for three languages: `docs/api/cpp/md/<Class>.md`, `docs/api/python/<Class>.md`, `docs/api/csharp/<Class>.md`. Maps types and names per language (e.g., `LineSegment3D const&` → `LineSegment3D` in Python, `LineSegment3D^` in C#; `std::optional<X>` → `X | None` in Python, `X^ (nullable)` in C#; PascalCase → `snake_case` for Python only). Cross-links every class-name occurrence in signatures, parameter columns, and prose, and appends a `**See also:** …` footer per file.
- Doxygen `@brief` / `@param` / `@return` comments added to ~155 public methods across `line3d`, `line_segment3d`, `ray3d` (3D linear primitives), their 2D counterparts (`line2d`, `line_segment2d`, `ray2d`), `triangle2d/3d`, `polyline2d/3d`, `polygon2d/3d`, `bbox2d/3d`, `plane`, and `wkt_parser`. Coverage targets methods in `#pragma region line operations` and `#pragma region Geometrical Operations` (plus Plane's `Geometrial Operations` / `Collection Operations`), plus the typical ray helpers (`IsAhead`, `IsBehind`, `ToLine`, `ProjectOnto`) and `WktParser::Open` / `Next` / `HasNext`.

### Changed

**C++ core**
- `Plane::SignedDistanceTo(Point3D)` — removed the `round()` call. The default precision was 0 decimal places, so the function used to snap the signed distance to the nearest integer. This silently broke `Plane::ProjectOnto` (which multiplies the signed distance by the normal) for points within ±0.5 of the plane.
- `Triangle3D::Intersects(Triangle3D)` — was a throwing stub; now `return Intersection(other).has_value()` like every other `Intersects` overload.
- `Plane::Intersection(Triangle3D)` — was a throwing stub; now delegates to `Triangle3D::Intersection(Plane)`, unwrapping its variant into `Plane::ReturnSet`'s alternatives (`Point3D` or `LineSegment3D`).
- `Line3D::Intersection(other, sc, tc)` — the internal three-argument overload (used as a shared kernel by `Ray3D` / `LineSegment3D` intersection) was removed from `Line3D`'s public surface. The math now lives in the free function `intersection_line_to_line` in `calc_utils3d`. The single-argument `Line3D::Intersection(Line3D)` is unchanged.

### Fixed

**C++ core**
- `Plane` header had a duplicate `bool Intersects(Line3D const&) const;` declaration; removed.
- `Ray3D::DistanceTo(Line3D)` — when a ray hit the line at a non-origin point (`Distance` returned `nullopt`), the function fell through to `other.DistanceTo(ORIGIN)` (perpendicular distance from the ray's origin to the line) instead of returning 0. Now returns 0 whenever `Distance` is `nullopt` (intersect or overlap), matching the docstring.
- `Ray3D::Distance(Ray3D)`, `LineSegment3D::Distance(Ray3D)`, `LineSegment3D::Distance(LineSegment3D)` — collinear-overlap cases (two primitives sharing a region of the same infinite line) returned a non-zero segment because the ray / segment parameter clamping ran *before* the closest-points equality check, corrupting `distance_line_to_line`'s "magic zero" in the parallel branch. Each function now detects collinearity (unclamped points coincide) and short-circuits to `nullopt` when the two primitives genuinely share a region (verified via `Contains` on endpoints/origins).

### Notes / known limitations

- `Triangle2D::Intersection(Line2D)` returns `nullopt` when *all* intersection points coincide with triangle vertices (the deliberate "touch along an edge ≠ intersection" rule at `triangle2d.cpp:144`). This propagates through `Triangle3D::Intersection(Plane)` and `Triangle3D::Intersection(Triangle3D)`: a plane that cuts the triangle exactly along an edge will report no intersection. Tests document this behavior rather than work around it.
- `Plane::Intersection(Plane)`, `Plane::Intersection(Line3D/Ray3D/LineSegment3D)`, and the propagated triangle variants still collapse the coplanar case to `nullopt` — the `ReturnSet` variant can't represent "infinite intersections." `IsCoplanar` is the workaround.
- `Triangle3D::Intersection(Triangle3D)`: when the two triangles' plane-intersection segments are collinear but disjoint, the segment-overlap branch falls through and throws `"unexpected type of intersection result"` instead of returning `nullopt`. The test `Triangle3DTest.IntersectionWTriangle` asserts this with `EXPECT_ANY_THROW` so it's documented.

### Tests

**C++ (`geompp_tests`)**
- `test_plane.cpp`: `IntersectionWRay`, `IntersectionWLineSegment`, `IntersectionWPlane`, `IntersectionWTriangle`, `IsParallelWLine/Ray/LineSegment`, `IsCoplanarWLine/Ray/LineSegment` added.
- `test_triangle3d.cpp`: `IntersectionWLine` replaced with a real test (interior hit, vertex hit, edge-midpoint, miss, parallel-above, coplanar). `IntersectionWRay`, `IntersectionWLineSegment`, `IntersectionWPlane`, `IntersectionWPlane_Symmetric`, `IntersectionWTriangle` added (including the documented disjoint-segments throw).
- `test_point2d.cpp` / `test_point3d.cpp`: `FromVector` — explicit construction, implicit conversion, and round-trip via `ToVector`.
- `test_calc_utils3d.cpp` (new file): `DistanceLineToLine_{Intersecting,Skew,ParallelDistinct,Overlap,ZeroLengthInputs}` and `IntersectionLineToLine_{Intersecting,SkewLinesNoIntersection,ParallelDistinctNoIntersection,OverlapNoIntersection,IntersectAtEndpointParams}`.
- `test_line3d.cpp`: `DistanceToLine3D`, `DistanceToRay3D`, `DistanceToLineSegment3D` — each covers crossing → 0, parallel-distinct → perp dist, collinear overlap → 0, and skew where applicable.
- `test_ray3d.cpp`: `DistanceToLine3D`, `DistanceToRay3D`, `DistanceToLineSegment3D` — same shape (crossing, parallel, skew, collinear overlap, plus back-to-back rays).
- `test_line_segment3d.cpp`: `Flip`, `DistanceToLine3D`, `DistanceToRay3D`, `DistanceToLineSegment3D` — including the 2D-top-cross / 3D-separated skew case (`LineSegment3D ↔ Ray3D` where the ray crosses the segment in XY but sits at z=5).

**Python (`geompp_python/tests`)**
- `TestPoint2D.test_construction_from_vector` and `TestPoint3D.test_construction_from_vector`.
- `TestPlane`: `test_intersection_with_ray`, `test_intersection_with_line_segment`, `test_intersection_with_plane`, `test_is_parallel`, `test_is_coplanar`.
- `TestTriangle3D`: `test_intersection_with_line`, `test_intersection_with_ray`, `test_intersection_with_line_segment`, `test_intersection_with_plane`, `test_intersection_with_triangle`.
- `TestLine3D` / `TestRay3D` / `TestLineSegment3D`: `test_distance_to_line3d`, `test_distance_to_ray3d`, `test_distance_to_segment3d` — covering crossing → 0, parallel → perp dist, and collinear-overlap → 0 (and `Distance(...)` returning `None`). `TestLineSegment3D.test_flip` added.

**C# (`geompp_csharp/tests`)**
- `Point2D` / `Point3D` — `CreateFromVector_CopiesComponents`, `CreateFromVector_RoundtripViaToVector`.
- `Plane` — `Intersects_*`/`Intersection_*` for `Ray3D`, `LineSegment3D`, `Plane`, `Triangle3D`; `IsParallel_*` and `IsCoplanar_*` for line/ray/segment.
- `Triangle3D` — `Intersects_Line3D_*`/`Intersection_Line3D_*` and the same for `Ray3D`, `LineSegment3D`, `Plane`, `Triangle3D` (12+ new tests, including parallel-above, coplanar, and the documented disjoint-segments throw).
- `Line3D` / `Ray3D` / `LineSegment3D` — `*_DistanceTo_*_{Crossing_IsZero,ParallelDistinct,Skew,Overlap_IsZero}` for every pair of 3D linear primitives (~29 new tests). `LineSegment3D_DistanceTo_Ray3D_SkewTopCross` covers the 2D-top-cross / 3D-separated case.

---