Euclid 0.52.0
Prefix Reserveddotnet add package Euclid --version 0.52.0
NuGet\Install-Package Euclid -Version 0.52.0
<PackageReference Include="Euclid" Version="0.52.0" />
<PackageVersion Include="Euclid" Version="0.52.0" />
<PackageReference Include="Euclid" />
paket add Euclid --version 0.52.0
#r "nuget: Euclid, 0.52.0"
#:package Euclid@0.52.0
#addin nuget:?package=Euclid&version=0.52.0
#tool nuget:?package=Euclid&version=0.52.0

Euclid
A comprehensive 2D and 3D geometry library in F# without dependencies, designed for precision engineering and computational design.
It runs on JavaScript too.
Features
While most 3D geometry libraries just use a generic type ( often called Vec3 or similar) containing three floats for both points and vectors,
Euclid makes a distinction to preserve semantic meaning and avoid accidental misuse.
A point represents a position in space, while a vector represents a direction and magnitude.
Even unitized vectors are a separate type. They are guaranteed to have a length of 1.0.
This helps to skip checks for zero length vectors in many operations.
🎯 Core Geometry Types
- Points (
Pt,Pnt), Vectors (Vc,Vec), Unit Vectors (UnitVc,UnitVec) - Lines, Planes, Boxes, Bounding Boxes, Polylines
- Rotations, Quaternions, 4x4 and rigid orthonormal 4x3 matrices
✨ Key Characteristics
- Zero dependencies: lightweight and self-contained
- Double precision: designed for CAD/manufacturing accuracy
- Immutable types: all primitive types are immutable for safety and ease of reasoning
- Cross-platform: compiles to .NET, JavaScript, TypeScript, Rust, Python via Fable
- Performance Focus: All small types are structs, functions are often inline and try to minimize the allocation.
- Interoperability: Can be used with Rhino3D, Revit, or any .NET environment, and also in JavaScript/TypeScript projects via Fable.
Coordinate System
This library uses a right-handed coordinate system with the Z-axis pointing up.
✅ Same as in: Rhino3D, Blender, SketchUp, Revit, AutoCAD
❌ Different from: Unity, Unreal Engine, Maya
This choice aligns with industry-standard CAD and architectural software.
Design Philosophy
Points vs Vectors
- Points (
Pt,Pnt): Positions in space - Vectors (
Vc,Vec): Directions and displacements
When a 4x4 transformation matrix is applied:
- Points: Undergo full transformation (rotation, scaling, translation)
- Vectors: Only rotate and scale (no translation)
This follows homogeneous coordinate conventions where vectors have w=0.
A vector in Euclid is just a direction with a magnitude. It has no location in space. Therefore transformations that depend on a location never apply to vectors:
- A vector cannot be translated. Translating a direction is meaningless; it stays the same vector.
- A vector cannot be rotated around a center point. Only location-bearing types
(points, lines, planes, rectangles, boxes, polylines) can rotate around a center.
Vectors only rotate around the origin (a pure rotation, e.g.
Vec.rotate,Vec.rotateOnZ).
The API enforces this: vector types (Vc, Vec, UnitVc, UnitVec) deliberately offer no
translate or rotateWithCenter members. (The two members that violated this rule,
Vec.rotateWithCenter and UnitVec.rotateWithCenter, are scheduled for removal.)
Naming Conventions
Core Types (2D and 3D):
| Type | 2D | 3D |
|---|---|---|
| Point | Pt |
Pnt |
| Vector | Vc |
Vec |
| Unit Vector | UnitVc |
UnitVec |
| Line | Line2D |
Line3D |
| Polyline | Polyline2D |
Polyline3D |
| Rectangle | Rect2D |
Rect3D |
| Bounding Rect/Box | BRect |
BBox |
3D-only Types:
| Type | Description |
|---|---|
PPlane |
Parametrized plane (origin + X/Y/Z axes) |
NPlane |
Normal plane (origin + normal vector) |
Box |
Oriented 3D box (origin + 3 axis vectors) |
Matrix |
4x4 transformation matrix |
RigidMatrix |
4x3 rigid transformation (rotation + translation only) |
Quaternion |
Unitized quaternion for 3D rotations |
Rotation2D |
2D rotation stored as sine/cosine pair |
Function Patterns
Functions are available in multiple forms:
// Static module function (lowercase, pipeable)
let normalized = Vec.unitize myVector
let moved = Pnt.translate myVec myPoint
// Instance method/property (uppercase)
let normalized = myVector.Unitized
let moved = myPoint.Transform myMatrix
// Pipeline style
myPoint
|> Pnt.translate (Vec(1, 0, 0))
|> Pnt.rotateOnZDeg 45.0
|> Pnt.transform myMatrix
Custom Operators
a + b // Point + Vector = Point, Vector + Vector = Vector
a - b // Point - Point = Vector, Point - Vector = Point
a * 2.0 // Scale vector or point
a / 2.0 // Divide (with zero-check)
a *** b // Dot product for vectors, matrix multiplication for matrices
-a // Negate a vector
API Documentation
Full API Reference: goswinr.github.io/Euclid
Platform Support
Thanks to Fable, Euclid can be used not only on .NET but also in JavaScript, TypeScript, Rust, and Python.
The CHECKED_EUCLID compiler flag
Several hot paths contain extra validation that is only compiled in when the DEBUG or the
CHECKED_EUCLID symbol is defined, for example:
NaNandInfinitychecks in thePt,Pnt,VcandVecconstructors- verifying that
UnitVcandUnitVecreally have length 1.0 - range checks in
Rotation2D.createFromSineandcreateFromCosine - index bounds checks in
Polyline2DandPolyline3D - validation in every unsafe
createUncheckedconstructor: perpendicular and right-handed axes inRect2D,Rect3D,BoxandPPlane, a unitized normal inNPlane, a unitizedQuaternion, and min values not bigger than max values inBRectandBBox
These checks are off by default in release builds because they can be expensive. Some primitive operations are up to 2.5 times slower with them enabled.
CHECKED_EUCLID exists so that the checks can be turned on independently of DEBUG.
This is mostly useful with Fable: Fable compiles the referenced libraries from source
rather than consuming a pre-built assembly, so the symbol actually reaches Euclid's code
and the checks end up in the generated JavaScript or TypeScript, even in a release build.
Enable it in the consuming project file:
<PropertyGroup>
<DefineConstants>$(DefineConstants);CHECKED_EUCLID</DefineConstants>
</PropertyGroup>
or on the command line:
dotnet fable --define CHECKED_EUCLID
On .NET the flag only has an effect when Euclid is built from source. The Euclid package on NuGet is pre-compiled without it.
Development
Use of AI and LLMs
All core function are are written by hand to ensure performance and correctness.
However, AI tools have been used for code review, typo and grammar checking in documentation
and to generate not all but many of the tests.
Prerequisites
- .NET SDK 10.0 or later (to have scoped warnings available)
- Node.js (only needed to run the .NET tests in JavaScript and TypeScript via Fable.Mocha)
Building from Source
git clone https://github.com/goswinr/Euclid.git
cd Euclid
To build the library and the tests for .NET
dotnet build --configuration Release
for JavaScript/TypeScript (compiles the .NET code to JavaScript via Fable)
cd Test
npm i
npm run build
cd ..
Testing
Tests run on both .NET and JavaScript with TypeScript build verification.
.NET Testing
run the tests from root. (not from within the Test folder)
dotnet run --project ./Test/Test.fsproj
JavaScript Testing
run
cd Test
npm i
npm test
cd ..
The test suite ensures cross-platform compatibility and verifies TypeScript type definitions.
Benchmarks
Some more complex algorithms are benchmarked in both .NET and Node JavaScript. See Test/Benchmarks/README.md.
Contributing
Contributions are welcome!
Changelog
See CHANGELOG.md for version history.
Related Projects
Euclid.Rhino - Rhino3D integration
License
Installation
Add Euclid to your F# project via NuGet:
dotnet add package Euclid
Or in F# scripting:
#r "nuget: Euclid"
Quick Start
open Euclid
// Create 3D points and vectors
let point1 = Pnt(1.0, 2.0, 3.0)
let point2 = Pnt(4.0, 5.0, 6.0)
let vector = Vec(1.0, 1.0, 0.0)
// Calculate distance
let distance = point1.DistanceTo point2
// Create and use unit vectors
let unitVec = vector.Unitized // returns a UnitVec
// Transform with 4x4 matrix
let matrix =
Matrix.createShear(3.0, 0, 0, 0, 0, 0)
*** // Combine transformations
Matrix.createRotationZ 45
point1
|> Pnt.translate vector
|> Pnt.scale 3.0
|> Pnt.transform matrix
Examples
Points and Vectors (2D)
open Euclid
// 2D points and vectors
let a = Pt(1.0, 2.0)
let b = Pt(4.0, 6.0)
// Distance between points
let dist = a.DistanceTo b // 5.0
// Midpoint
let mid = Pt.midPt a b // Pt(2.5, 4.0)
// 2D vectors
let v = Vc(3.0, 4.0)
let len = v.Length // 5.0
let half = v.Half // Vc(1.5, 2.0)
let perp = v.Rotate90CCW // perpendicular vector
// Point + vector arithmetic
let moved = a + v // Pt(4.0, 6.0)
let diff = b - a // Vc(3.0, 4.0) : point - point = vector
// Unit vectors (guaranteed length 1.0)
let dir = v.Unitized // UnitVc(0.6, 0.8)
let angle = v.Direction360 // angle in degrees from X-axis
// Dot and cross product
let dot = v.Dot(Vc(1.0, 0.0)) // 3.0
let cross = v.Cross(Vc(1.0, 0.0)) // -4.0 (signed area in 2D)
Points and Vectors (3D)
open Euclid
// 3D points and vectors
let p1 = Pnt(1.0, 2.0, 3.0)
let p2 = Pnt(4.0, 6.0, 3.0)
// Distance, midpoint, lerp
let dist = p1.DistanceTo p2 // 5.0
let mid = Pnt.midPt p1 p2 // Pnt(2.5, 4.0, 3.0)
let lerp = Pnt.divPt(p1, p2, 0.25) // 25% from p1 towards p2
// 3D vectors
let v1 = Vec(1.0, 0.0, 0.0)
let v2 = Vec(0.0, 1.0, 0.0)
// Cross product and dot product
let normal = v1.Cross v2 // Vec(0, 0, 1)
let dot = v1.Dot v2 // 0.0
// Angle between vectors
let angle = Vec.angle180 v1 v2 // 90.0 degrees
// Unit vectors
let dir = Vec(3.0, 4.0, 0.0).Unitized // UnitVec(0.6, 0.8, 0.0)
let isPerp = dir.IsPerpendicularTo UnitVec.Zaxis // true
// Convert between 2D and 3D
let pt2d = p1.AsPt // Pt(1.0, 2.0) - drops Z
let pt3d = Pt(1.0, 2.0).WithZ 5.0 // Pnt(1.0, 2.0, 5.0)
Lines
open Euclid
// Create lines from points
let ln2d = Line2D(Pt(0, 0), Pt(10, 0))
let ln3d = Line3D(Pnt(0, 0, 0), Pnt(10, 5, 3))
// Or from coordinates
let ln = Line2D(0.0, 0.0, 10.0, 5.0)
// Line properties
let len = ln2d.Length // 10.0
let midPt = ln2d.Mid // Pt(5, 0)
let dir = ln3d.Direction // UnitVec of line direction
let tang = ln3d.Tangent // UnitVec (same as Direction)
// Evaluate at parameter (0.0 = start, 1.0 = end)
let quarterPt = ln2d.EvaluateAt 0.25 // Pt(2.5, 0)
// Extend, shrink, reverse
let longer = ln2d.Extend(2.0, 3.0) // extend 2 at start, 3 at end
let shorter = ln2d.Shrink(1.0, 1.0) // shrink 1 from each end
let flipped = ln2d.Reversed // swap start and end
// Closest point queries
let testPt = Pt(5.0, 3.0)
let closest = ln2d.ClosestPoint testPt // Pt(5, 0) - clamped to segment
let param = ln2d.ClosestParameter testPt // 0.5
// Line-line relationships
let other = Line2D(Pt(5, -5), Pt(5, 5))
let isParallel = ln2d.IsParallelTo other // false
let isPerp = ln2d.IsPerpendicularTo other // true
// Intersection (returns option)
let hit = Line2D.tryIntersect ln2d other // Some (Pt(5, 0))
// Offset a line
let offsetLn = Line2D.offset 2.0 ln2d // offset by 2.0 to the left
// Move a line
let movedLn = ln2d.Move(Vc(0.0, 5.0)) // translate by vector
Line Intersections (Discriminated Unions)
For detailed intersection analysis, the XLine2D and XLine3D modules return discriminated union types
that let you handle every case explicitly:
open Euclid
let lineA = Line2D(Pt(0, 0), Pt(10, 0))
let lineB = Line2D(Pt(5, -5), Pt(5, 5))
// Finite segment intersection - returns XPt discriminated union
match XLine2D.getIntersection(lineA, lineB) with
| XLine2D.XPt.Intersect pt -> printfn $"Lines cross at {pt}"
| XLine2D.XPt.Apart -> printfn "Segments don't reach each other"
| XLine2D.XPt.Parallel -> printfn "Lines are parallel"
| XLine2D.XPt.TooShortA -> printfn "Line A is too short"
| XLine2D.XPt.TooShortB -> printfn "Line B is too short"
| XLine2D.XPt.TooShortBoth -> printfn "Both lines are too short"
// Ray intersection (extends lines infinitely)
match XLine2D.getRayIntersection(lineA, lineB) with
| XLine2D.XRay.Intersect pt -> printfn $"Rays meet at {pt}"
| XLine2D.XRay.Parallel -> printfn "Rays are parallel"
| _ -> ()
// 3D line relationships include skew detection
let ln3A = Line3D(Pnt(0, 0, 0), Pnt(10, 0, 0))
let ln3B = Line3D(Pnt(5, 0, 5), Pnt(5, 10, 5))
match XLine3D.getIntersection(ln3A, ln3B) with
| XLine3D.XPnt.Intersect pt -> printfn $"Lines intersect at {pt}"
| XLine3D.XPnt.Skew (ptA, ptB, d) -> printfn $"Closest approach: {d} between {ptA} and {ptB}"
| XLine3D.XPnt.Apart -> printfn "Segments don't reach each other"
| XLine3D.XPnt.Parallel -> printfn "Lines are parallel"
| _ -> ()
Planes
open Euclid
// Normal plane (origin + normal direction)
let npl = NPlane(Pnt(0, 0, 5), UnitVec.Zaxis)
// Signed distance from plane (positive = same side as normal)
let d = npl.DistanceToPnt (Pnt(3, 4, 8)) // 3.0
let d2 = npl.DistanceToPnt (Pnt(3, 4, 2)) // -3.0
// Project a point onto the plane
let proj = npl.ClosestPoint (Pnt(3, 4, 8)) // Pnt(3, 4, 5)
// Angle between plane and vector (0 = parallel, 90 = perpendicular)
let ang = npl.Angle90ToVec (Vec(1, 0, 0)) // 0.0 (vector lies in plane)
// Parametrized plane (full coordinate frame with X, Y, Z axes)
let ppl = PPlane.createThreePoints
(Pnt(0, 0, 0)) // origin
(Pnt(10, 0, 0)) // point on X-axis
(Pnt(0, 10, 0)) // point on Y-axis side
Rectangles
open Euclid
// 2D rectangle from origin, X-direction vector, and Y-size
let rect = Rect2D.createFromXVectorAndWidth(Pt(0, 0), Vc(10, 0), 5.0)
// From direction and sizes
let dir = UnitVc.rotate 45.0 UnitVc.Xaxis
let rotRect = Rect2D.createFromDirectionAndSizes(Pt(0, 0), dir, 10.0, 5.0)
// Rectangle properties
let area = rect.Area
let cx = rect.SizeX
let cy = rect.SizeY
let center = rect.Center
let c0 = rect.Origin // origin corner
let c2 = rect.FarCorner // diagonally opposite corner
// Point evaluation (u, v parameters from 0 to 1)
let midPoint = rect.EvaluateAt(0.5, 0.5) // center point
Bounding Boxes
open Euclid
// 3D axis-aligned bounding box from points
let points = [Pnt(0, 0, 0); Pnt(10, 5, 3); Pnt(-2, 8, 1)]
let bbox = BBox.createFromSeq points
// BBox properties
let size = bbox.SizeX, bbox.SizeY, bbox.SizeZ // 12.0, 8.0, 3.0
let minPt = bbox.MinPnt // Pnt(-2, 0, 0)
let maxPt = bbox.MaxPnt // Pnt(10, 8, 3)
let center = bbox.Center // Pnt(4, 4, 1.5)
let vol = bbox.Volume // 288.0
// Create from center and sizes
let box2 = BBox.createFromCenter(Pnt(0, 0, 0), 10.0, 10.0, 10.0)
// Expand bounding box
let bigger = bbox.Expand 1.0 // expand by 1.0 on all sides
// Check containment
let inside = bbox.ContainsPnt(Pnt(5, 4, 2)) // true
// Union of two bounding boxes
let combined = bbox.Union box2
Polylines
open Euclid
// Create a 2D polyline
let pl2d = Polyline2D.createFromPts [Pt(0, 0); Pt(10, 0); Pt(10, 5); Pt(0, 5)]
// Polyline properties
let len = pl2d.Length // total length
let count = pl2d.PointCount // 4
let segs = pl2d.SegmentCount // 3
// Access segments as Line2D
let firstSeg = pl2d.FirstSegment // Line2D(Pt(0,0), Pt(10,0))
let seg = pl2d.GetSegment(1) // Line2D(Pt(10,0), Pt(10,5))
// Modify (polylines are mutable)
pl2d.SetPt (2, Pt(12, 5)) // move a vertex
let copy = pl2d.Duplicate() // deep copy
// 3D polyline
let pl3d = Polyline3D.createFromPts [Pnt(0,0,0); Pnt(10,0,0); Pnt(10,5,3)]
let totalLen = pl3d.Length
Transformations with Matrices
open Euclid
let pt = Pnt(5.0, 0.0, 0.0)
// Translation
let m1 = Matrix.createTranslation(10.0, 0.0, 0.0)
// Rotation (angle in degrees)
let m2 = Matrix.createRotationZ 90.0 // 90 degrees around Z
let m3 = Matrix.createRotationX 45.0 // 45 degrees around X
// Rotation around arbitrary axis
let m4 = Matrix.createRotationAxis(UnitVec.Zaxis, 30.0)
// Rotation around axis at a specific center point
let m5 = Matrix.createRotationAxisCenter(Vec(0, 0, 1), Pnt(5, 5, 0), 45.0)
// Scale
let m6 = Matrix.createScale(2.0, 2.0, 1.0) // scale X and Y by 2
// Shear
let m7 = Matrix.createShear(0.5, 0, 0, 0, 0, 0) // shear XY by 0.5
// Combine transformations with *** operator
let combined = m1 *** m2 *** m6
// Apply to points and vectors
let pt2 = pt.Transform combined // full transform (rotate, scale, translate)
let rigid = RigidMatrix.createRotationZ 90.0
let v = Vec(1, 0, 0)
let v2 = v.TransformRigid rigid // rotate without translation
// Functional pipeline style
let result =
pt
|> Pnt.translate (Vec(10, 0, 0))
|> Pnt.rotateOnZDeg 90.0
|> Pnt.scale 2.0
|> Pnt.transform combined
// Matrix from plane to plane
let fromPlane = PPlane.createThreePoints (Pnt(0,0,0)) (Pnt(1,0,0)) (Pnt(0,1,0))
let toPlane = PPlane.createThreePoints (Pnt(5,5,0)) (Pnt(6,5,0)) (Pnt(5,6,0))
let planeXform = Matrix.createPlaneToPlane(fromPlane, toPlane)
// Mirror about a plane
let mirror = Matrix.createMirror fromPlane
Quaternion Rotations
open Euclid
// Create quaternion from axis and angle
let q1 = Quaternion.createFromDegree(Vec(0, 0, 1), 90.0) // 90 degrees around Z
let q2 = Quaternion.createFromRadians(UnitVec.Xaxis, 1.5708) // Pi/2 around X
// Rotate from one direction to another
let q3 = Quaternion.createVecToVec(UnitVec.Xaxis, UnitVec.Yaxis) // X -> Y
// Quaternion properties
let angle = q1.AngleInDegrees // 90.0
let axis = q1.Axis // Vec close to (0, 0, 1)
let inv = q1.Inverse // reverse rotation
// Compose rotations by multiplication
let q4 = q1 * q2 // first q2, then q1
// Apply to points and vectors
let rotated = Pnt.rotate q1 (Pnt(1, 0, 0)) // Pnt(0, 1, 0)
// Convert quaternion to matrix
let mat = Matrix.createFromQuaternion q1
Point Cloud Operations
open Euclid
let cloud = ResizeArray [Pnt(0,0,0); Pnt(1,0,0); Pnt(5,5,5); Pnt(10,10,10)]
// Find closest point to a test point
let nearest = Points3D.closestPoint(cloud, Pnt(1.1, 0.1, 0.0)) // Pnt(1,0,0)
let idx = Points3D.closestPointIdx(cloud, Pnt(1.1, 0.1, 0.0)) // 1
// Closest points between two sets
let setA = ResizeArray [Pnt(0,0,0); Pnt(10,10,10)]
let setB = ResizeArray [Pnt(1,0,0); Pnt(20,20,20)]
let (iA, iB) = Points3D.closestPointsIdx(setA, setB) // (0, 0)
// Most distant point
let farthest = Points3D.mostDistantPoint(cloud, setB)
// center of points
let center = Points3D.center cloud // Pnt(4, 3.75, 3.75)
Error Handling
Euclid uses descriptive custom exceptions instead of silent failures:
open Euclid
// Unitizing a zero vector raises EuclidException
try
let bad = Vec(0, 0, 0).Unitized // fails: can't unitize zero vector
with e ->
printfn $"Caught: {e.Message}"
// Division by near-zero raises EuclidDivByZeroException
// NaN or Infinity in inputs raises EuclidException
// Tolerances are conservative for precision engineering:
// - zeroLengthTolerance = 1e-12 (for divisions, unitizing)
// - isTooSmall = 1e-6 (general smallness check)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Framework | net472 is compatible. net48 was computed. net481 was computed. |
-
.NETFramework 4.7.2
- FSharp.Core (>= 6.0.7)
- System.Text.Json (>= 4.6.0)
-
net6.0
- FSharp.Core (>= 6.0.7)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Euclid:
| Package | Downloads |
|---|---|
|
Euclid.Rhino
Utilities to use types from Euclid in Rhino3d |
|
|
Euclid.BVH
A Bounding Volume Hierarchy (BVH) for fast spatial queries on the Euclid geometry library. Find closest lines and pairs of close lines among thousands of 3D lines. |
|
|
Euclid.Kontur
Boolean operations on Euclid 2D polygons: union, intersection, difference, xor, simplification and union of many shapes. Floating-point coordinates with an absolute tolerance, for .NET and Fable. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.52.0 | 304 | 10/10/2026 |
| 0.51.0 | 575 | 7/28/2026 |
| 0.50.0 | 184 | 7/16/2026 |
| 0.42.0 | 465 | 6/20/2026 |
| 0.41.0 | 224 | 6/17/2026 |
| 0.40.0 | 199 | 6/13/2026 |
| 0.30.1 | 190 | 5/23/2026 |
| 0.30.0 | 174 | 5/23/2026 |
| 0.20.0 | 531 | 2/8/2026 |
| 0.20.0-beta3 | 181 | 1/24/2026 |
| 0.20.0-beta2 | 575 | 1/23/2026 |
| 0.20.0-beta1 | 154 | 1/17/2026 |
| 0.16.0 | 726 | 6/30/2025 |
| 0.15.0 | 346 | 6/14/2025 |
| 0.14.0 | 725 | 5/24/2025 |
| 0.13.0 | 388 | 3/15/2025 |
| 0.12.0 | 567 | 11/27/2024 |
| 0.11.1 | 602 | 10/1/2024 |
| 0.11.0 | 245 | 9/25/2024 |
| 0.10.0 | 264 | 9/19/2024 |
### Added
- Added `with`-prefixed replacements for all members that had a `set` prefix but returned a new value instead of mutating: `Line2D.withStart/withFrom/withEnd/withTo`, `Line3D.withStart/withFrom/withEnd/withTo`, `PPlane.withOrigin/withOriginX/withOriginY/withOriginZ`, and `Quaternion.WithAngleInRadians/WithAngleInDegrees` (plus the static `withAngleInRadians`/`withAngleInDegrees`).
- Added `Box.DoesIntersectPlane` and `Box.IntersectPlane`, plus `BBox.DoesIntersectPlane` and `BBox.IntersectPlane`, and their static counterparts, to test for and get the intersection polygon points of a box with an `NPlane`.
- Added `Box.tryGetBoxBetween` and `Box.getBoxBetween` to get the box filling the gap between two aligned boxes.
- Added `Box.areBoxesAligned`, `Box.getBoxesAlignment`, and `Box.failIfBoxesNotAlignedTo` to check if the axes of two boxes are parallel and equally oriented.
- Added `Box.withSizeX`, `Box.withSizeY`, `Box.withSizeZ`, and `Box.withOrigin`.
- Added `Matrix.createPerspectiveAlongNegZ` and `Matrix.createPerspectiveAlongPosY` for perspective cameras looking along -Z with +Y up, or +Y with +Z up, respectively.
- Added `Matrix.createLookAt` and `Matrix.createPerspectiveLookAt` for cameras with an arbitrary position, target, and preferred up vector.
- Added `Matrix.createLookAtZUp` and `Matrix.createPerspectiveLookAtZUp` to keep world +Z up without camera roll while pitching toward the target. Vertical and nearly vertical views use world +Y as the fallback up direction.
- `Rect2D`, `Rect3D`, `BRect`, `Box`, `BBox`, and `FreeBox` now have a reverse-direction instance and static member for every named edge (e.g. `Edge10` next to `Edge01`, `Edge23` next to `Edge32`), so every edge can be addressed in either direction.
- Added `Line2D.pointAtDistanceFromEnd` and `Line3D.pointAtDistanceFromEnd`, finding a point at a given distance from the line end, going towards the start.
- Added `Polyline2D.checkForDuplicatePoints` and `Polyline3D.checkForDuplicatePoints`, which return the original polyline when unchanged or a cleaned polyline and reported duplicate points.
- Added `Polyline3D.TryPlane` and `Polyline3D.IsPlanar`, plus the static `tryPlane` and `isPlanar`, to get the `NPlane` of a planar Polyline3D within a tolerance.
- Added `Polyline3D.AveragePlane` and `Polyline3D.TryAveragePlane`, plus the static `averagePlane` and `tryAveragePlane`, to get the average `NPlane` of a Polyline3D even when its points are not planar.
- Added `Polyline3D.TryAverageNormal` and the static `tryAverageNormal`, returning `None` instead of failing where `AverageNormal` finds no normal.
- The unsafe internal constructors of `NPlane`, `PPlane`, `Box`, `BRect`, and `BBox`, and therefore all their `createUnchecked` members, now verify their input when compiled in DEBUG mode or with the `CHECKED_EUCLID` symbol defined. This covers unitized normals and axes, perpendicular and right-handed frames, `NaN` and `Infinity` values, and min values not being bigger than max values. The other unsafe constructors already did this.
- Documented the `CHECKED_EUCLID` compiler flag in the Readme.
- Added `Rect3D.translateLocalZ`, an alias of `Rect3D.offsetZ`, for naming consistency with `Box.translateLocalX/Y/Z` and `Rect3D.translateLocalX/Y`.
### Removed
- The obsolete numeric `Edge0`-`Edge11` instance members on `Box` and `FreeBox` (deprecated aliases for the endpoint-named edges) have been removed; `Edge10` is now used for the reverse of `Edge01` instead.
- `Polyline2D.iterSkipLast`, `Polyline2D.iterPtSkipLast`, `Polyline3D.iterSkipLast`, and `Polyline3D.iterPtSkipLast` have been removed.
### Changed
- The `System.Text.Json` converter types (e.g. `BBoxJsonConverter`) are now internal. Serialization with `System.Text.Json` works as before, since the types still reference their converters via attributes.
- Builds use the .NET SDK's built-in SourceLink instead of the `Microsoft.SourceLink.GitHub` package.
- `Polyline3D.AverageNormal` now fails if no normal can be found, instead of silently returning an almost zero length vector. This is the case if the Polyline3D has less than 3 points, if all points are in one line, if the cross products cancel each other out on a self intersecting shape, or if the Polyline3D is very small. Use `Polyline3D.TryAverageNormal` to get `None` instead of an exception.
- The `CHECK_EUCLID` compiler symbol was renamed to `CHECKED_EUCLID`.
- Pinned the `System.Text.Json` package reference (net472 only) to the lowest version providing the required APIs, `4.6.0`, instead of tracking latest, and excluded it from Dependabot version updates.
- Clarified the docstrings of `Box.MoveX/Y/Z`, `Rect2D.MoveX/Y`, and `Rect3D.MoveX/Y/Z` (instance and static) to state that they move along the **world** X/Y/Z axes, not the shape's own (possibly rotated) local axes. Use `translateLocalX/Y/Z` for local-axis moves.
### Deprecated
- All members with a `set` prefix that return a new value instead of mutating are obsolete, use the `with`-prefixed member of the same name instead. The `set` prefix suggested in-place mutation, which these never did. This affects `Pt.setX/setY`, `Pnt.setX/setY/setZ`, `Vc.setX/setY`, `Vec.setX/setY/setZ`, `Line2D.setStart/setFrom/setEnd/setTo`, `Line3D.setStart/setFrom/setEnd/setTo`, `PPlane.setOrigin/setOriginX/setOriginY/setOriginZ`, and `Quaternion.SetAngleInRadians/SetAngleInDegrees` (plus the static `setAngleInRadians`/`setAngleInDegrees`). The genuinely mutating `Set` members on `Polyline2D`, `Polyline3D`, `FreeBox`, and `ResizeArr` are unchanged.
- `Matrix.createPerspective` is obsolete. Use `Matrix.createPerspectiveAlongNegZ` instead; the old name remains a compatibility alias with the same projection behavior.
- `Box.translate`, `Rect2D.translate`, `Rect3D.translate`, and `PPlane.translate` are obsolete. They were plain aliases for `.move` (a world-space vector translation), which is ambiguous alongside the local-axis `translateLocalX/Y/Z` members on the same types. Use `.move` (or `PPlane.translateBy`) for a world-space vector, or `.translateLocalX/Y/Z` to move along the shape's own axes.
### Fixed
- Packaging: the Fable source files are now packed from the Src folder instead of the repository root, so the package no longer ships test sources or generated obj files.
- `XLineXY.sqDistance` and `XLineXYZ.sqDistance` (and the 2D and 3D line distance functions built on them) now return the correct squared distance for nearly parallel segments, where numeric cancellation could give a wrong result.
- `AsFSharpCode` of all types now formats floats with the invariant culture and round-trip precision, so the generated code compiles and recreates the exact values on any system locale.