Geometry Library in C# — The Unit Vector

Saturday, April 4, 2026

In the previous articles, we built points, vectors, and operators. Our vectors can represent any displacement: a small step, a large leap, or even a zero displacement. But in many situations, you don't need a displacement — you need a direction.

Why a separate type?

The direction of a line, the normal of a plane, the axis of a rotation — all these concepts share one property: length is meaningless. Only the orientation matters. By convention, a direction is represented by a vector of length 1, called a unit vector.

We could settle for a normalized Vector2d, but nothing would guarantee it's actually unit-length. We'd have to check at every use, or trust the caller — both fragile approaches.

A dedicated UnitVector2d type solves the problem at the root:

  • The constructor verifies that the vector is unit-length.
  • The method signature expresses intent: when a method accepts a UnitVector2d, it requires a direction, not an arbitrary displacement.
  • Operations that would break the invariant (scalar multiplication, addition) are either not defined or return a Vector2d.

It's the same principle as separating Point and Vector: the type system prevents mistakes from compiling.

The UnitVector2d struct

public readonly struct UnitVector2d
{
    public static UnitVector2d XAxis { get; } = new(1.0, 0.0);

    public static UnitVector2d YAxis { get; } = new(0.0, 1.0);

    public double X { get; }

    public double Y { get; }

    public UnitVector2d(double x, double y, double tolerance = 1e-10)
    {
        double squaredLength = x * x + y * y;

        if (Math.Abs(squaredLength - 1.0) > tolerance)
            throw new ArgumentException("The components do not form a unit vector.");

        X = x;
        Y = y;
    }
}

No Length property: it would always be 1 by construction, so exposing it would add nothing.

The constructor verifies that \(x^2 + y^2 \approx 1\) within a tolerance. We compare the squared length to 1 rather than the length to 1 — once again, to avoid an unnecessary Math.Sqrt. If the provided vector isn't unit-length, an exception is thrown immediately, preventing any downstream contamination.

Creating a unit vector from a vector

The most common case is extracting the direction of an existing vector. Until now, we used GetNormal() which returned a Vector2d. We'll add a static factory method that returns a UnitVector2d directly:

public static UnitVector2d CreateFromVector(Vector2d vector, double tolerance = 1e-10)
{
    double length = vector.Length;

    if (length < tolerance)
        throw new ArgumentException("Cannot create a unit vector from a zero-length vector.");

    return new UnitVector2d(vector.X / length, vector.Y / length);
}

Example:

var velocity = new Vector2d(3.0, 4.0);
UnitVector2d direction = UnitVector2d.CreateFromVector(velocity); // (0.6, 0.8)

Implicit conversion to Vector2d

A unit vector is a special case of a vector. We should be able to use it anywhere a Vector2d is expected — to compute a dot product, pass it to an operator, etc. Implicit conversion makes this transparent:

public static implicit operator Vector2d(UnitVector2d unit)
{
    return new Vector2d(unit.X, unit.Y);
}

This conversion is safe: no information is lost, we're simply widening the type. The reverse conversion (from Vector2d to UnitVector2d) is intentionally not defined — it requires normalization and validation, so it must be explicit via CreateFromVector.

UnitVector2d direction = UnitVector2d.XAxis;
Vector2d displacement = direction * 5.0; // Implicit conversion, then multiplication
double dot = new Vector2d(1.0, 1.0).DotProduct(direction); // Implicit conversion

Negation

Reversing a direction always yields a direction. This is the only arithmetic operation that preserves the unit-length invariant:

public static UnitVector2d operator -(UnitVector2d vector)
{
    return new UnitVector2d(-vector.X, -vector.Y);
}

Other operations (+, *, /) are not defined on UnitVector2d: they would produce a vector with a length other than 1. Thanks to implicit conversion, you can still write direction * 5.0 — C# first converts to Vector2d, then applies the multiplication.

The perpendicular vector

In 2D, the perpendicular vector to a unit vector is also a unit vector (a 90° rotation doesn't change the length):

public UnitVector2d GetPerpendicularVector()
{
    return new UnitVector2d(-Y, X);
}

The complete UnitVector2d code

public readonly struct UnitVector2d
{
    public static UnitVector2d XAxis { get; } = new(1.0, 0.0);

    public static UnitVector2d YAxis { get; } = new(0.0, 1.0);

    public double X { get; }

    public double Y { get; }

    public UnitVector2d(double x, double y, double tolerance = 1e-10)
    {
        double squaredLength = x * x + y * y;

        if (Math.Abs(squaredLength - 1.0) > tolerance)
            throw new ArgumentException("The components do not form a unit vector.");

        X = x;
        Y = y;
    }

    public static UnitVector2d CreateFromVector(Vector2d vector, double tolerance = 1e-10)
    {
        double length = vector.Length;

        if (length < tolerance)
            throw new ArgumentException("Cannot create a unit vector from a zero-length vector.");

        return new UnitVector2d(vector.X / length, vector.Y / length);
    }

    public UnitVector2d GetPerpendicularVector()
    {
        return new UnitVector2d(-Y, X);
    }

    public static UnitVector2d operator -(UnitVector2d vector)
    {
        return new UnitVector2d(-vector.X, -vector.Y);
    }

    public static implicit operator Vector2d(UnitVector2d unit)
    {
        return new Vector2d(unit.X, unit.Y);
    }
}

Going 3D with UnitVector3d

The 3D version follows the same pattern, with the same adjustments as for Vector3d:

  • Three static properties XAxis, YAxis, ZAxis.
  • GetPerpendicularVector uses the Arbitrary Axis Algorithm from the DXF specification, and returns a UnitVector3d.
  • Implicit conversion to Vector3d.
public readonly struct UnitVector3d
{
    public static UnitVector3d XAxis { get; } = new(1.0, 0.0, 0.0);

    public static UnitVector3d YAxis { get; } = new(0.0, 1.0, 0.0);

    public static UnitVector3d ZAxis { get; } = new(0.0, 0.0, 1.0);

    public double X { get; }

    public double Y { get; }

    public double Z { get; }

    public UnitVector3d(double x, double y, double z, double tolerance = 1e-10)
    {
        double squaredLength = x * x + y * y + z * z;

        if (Math.Abs(squaredLength - 1.0) > tolerance)
            throw new ArgumentException("The components do not form a unit vector.");

        X = x;
        Y = y;
        Z = z;
    }

    public static UnitVector3d CreateFromVector(Vector3d vector, double tolerance = 1e-10)
    {
        double length = vector.Length;

        if (length < tolerance)
            throw new ArgumentException("Cannot create a unit vector from a zero-length vector.");

        return new UnitVector3d(vector.X / length, vector.Y / length, vector.Z / length);
    }

    public UnitVector3d GetPerpendicularVector()
    {
        Vector3d asVector = this;

        Vector3d reference = Math.Abs(X) < 1.0 / 64.0 && Math.Abs(Y) < 1.0 / 64.0
            ? new Vector3d(0.0, 1.0, 0.0)
            : new Vector3d(0.0, 0.0, 1.0);

        Vector3d cross = reference.CrossProduct(asVector);
        return CreateFromVector(cross);
    }

    public static UnitVector3d operator -(UnitVector3d vector)
    {
        return new UnitVector3d(-vector.X, -vector.Y, -vector.Z);
    }

    public static implicit operator Vector3d(UnitVector3d unit)
    {
        return new Vector3d(unit.X, unit.Y, unit.Z);
    }
}

Note that GetPerpendicularVector doesn't need to check for zero length here: a UnitVector3d always has a length of 1 by construction.

Impact on existing types

Introducing UnitVector has ripple effects on our existing types. Some methods that returned a Vector should now return a UnitVector:

Type Method Before After
Vector2d GetNormal() Vector2d UnitVector2d
Vector3d GetNormal() Vector3d UnitVector3d
Vector3d GetPerpendicularVector() Vector3d UnitVector3d

Thanks to implicit conversion, this change is backward-compatible: existing code that stored the result in a Vector2d continues to compile without modification.

// Both lines work after the change:
UnitVector2d direction = new Vector2d(3.0, 4.0).GetNormal(); // Precise type
Vector2d alsoWorks = new Vector2d(3.0, 4.0).GetNormal();     // Implicit conversion

What's next

Our library now distinguishes three concepts: positions (points), displacements (vectors), and directions (unit vectors). In the next article, we'll tackle geometric transformations: translations, rotations, and scaling.

On the same topic