Geometry Library in C# — Operators
Saturday, April 4, 2026In the previous articles, we built the Point and Vector types. We can already create points, compute distances, and normalize vectors. But the code remains verbose. Compare:
// Without operators
var displacement = new Vector2d(a, b);
var midpoint = new Point2d(a.X + displacement.X * 0.5, a.Y + displacement.Y * 0.5);
// With operators
Point2d midpoint = a + (b - a) * 0.5;
The second form is more readable and maps exactly to mathematical notation. C# lets us overload operators for our custom types — let's take advantage of it.
Which operators, and between which types?
Before writing code, let's think about which operations have geometric meaning. Some combinations are natural, others are nonsensical, and the type system should prevent the latter from compiling.
Vector operations
A vector represents a displacement. You can combine displacements:
| Expression | Result | Meaning |
|---|---|---|
Vector + Vector |
Vector |
Chain two displacements |
Vector - Vector |
Vector |
Difference between two displacements |
-Vector |
Vector |
Reverse direction |
Vector * scalar |
Vector |
Scale a displacement up or down |
scalar * Vector |
Vector |
Same (commutativity) |
Vector / scalar |
Vector |
Scale a displacement down |
Point-vector operations
A point is a position, a vector is a displacement. Combining them yields:
| Expression | Result | Meaning |
|---|---|---|
Point + Vector |
Point |
Translate a point |
Point - Vector |
Point |
Translate a point in reverse |
Point - Point |
Vector |
The displacement from one to the other |
What should NOT compile
Adding two points has no geometric meaning. Multiplying a point by a scalar doesn't either. By not defining these operators, the compiler will reject them automatically — that's the whole point of separating Point and Vector into distinct types.
var result = a + b; // Compilation error if a and b are Point2d ✓
Vector2d operators
Let's start with the simplest operators — those involving only vectors:
public static Vector2d operator +(Vector2d left, Vector2d right)
{
return new Vector2d(left.X + right.X, left.Y + right.Y);
}
public static Vector2d operator -(Vector2d left, Vector2d right)
{
return new Vector2d(left.X - right.X, left.Y - right.Y);
}
public static Vector2d operator -(Vector2d vector)
{
return new Vector2d(-vector.X, -vector.Y);
}
public static Vector2d operator *(Vector2d vector, double scalar)
{
return new Vector2d(vector.X * scalar, vector.Y * scalar);
}
public static Vector2d operator *(double scalar, Vector2d vector)
{
return vector * scalar;
}
public static Vector2d operator /(Vector2d vector, double scalar)
{
return new Vector2d(vector.X / scalar, vector.Y / scalar);
}
A few things to note:
- The
*operator is defined twice to support bothvector * 2.0and2.0 * vector. C# doesn't infer commutativity automatically. The second version delegates to the first to avoid duplication. - The unary
-operator allows writing-vectorto reverse direction. - The
/operator doesn't check for division by zero: C# producesdouble.PositiveInfinityordouble.NaNin that case, which is standarddoublebehavior and will be caught downstream.
Example:
var a = new Vector2d(2.0, 3.0);
var b = new Vector2d(1.0, -1.0);
Vector2d sum = a + b; // (3, 2)
Vector2d scaled = a * 2.0; // (4, 6)
Vector2d reversed = -a; // (-2, -3)
Vector2d half = a / 2.0; // (1, 1.5)
Point2d operators
Point operators always involve a vector — this is what guarantees geometric consistency:
public static Point2d operator +(Point2d point, Vector2d vector)
{
return new Point2d(point.X + vector.X, point.Y + vector.Y);
}
public static Point2d operator -(Point2d point, Vector2d vector)
{
return new Point2d(point.X - vector.X, point.Y - vector.Y);
}
public static Vector2d operator -(Point2d left, Point2d right)
{
return new Vector2d(right.X - left.X, right.Y - left.Y);
}
The last operator is the most important: subtracting two points produces a vector, not a point. It has exactly the same semantics as the Vector2d(Point2d from, Point2d to) constructor we defined in the previous article, but with a more natural syntax.
Pay attention to the order: b - a yields the vector going from a to b. This follows the mathematical convention \(\vec{AB} = B - A\).
Example:
var origin = Point2d.Origin;
var direction = new Vector2d(1.0, 0.0);
Point2d translated = origin + direction; // (1, 0)
Point2d backAgain = translated - direction; // (0, 0)
var a = new Point2d(1.0, 2.0);
var b = new Point2d(4.0, 6.0);
Vector2d displacement = b - a; // (3, 4)
What about 3D?
The 3D operators are identical to their 2D counterparts, with an added Z component. For Vector3d:
public static Vector3d operator +(Vector3d left, Vector3d right)
{
return new Vector3d(left.X + right.X, left.Y + right.Y, left.Z + right.Z);
}
public static Vector3d operator -(Vector3d left, Vector3d right)
{
return new Vector3d(left.X - right.X, left.Y - right.Y, left.Z - right.Z);
}
public static Vector3d operator -(Vector3d vector)
{
return new Vector3d(-vector.X, -vector.Y, -vector.Z);
}
public static Vector3d operator *(Vector3d vector, double scalar)
{
return new Vector3d(vector.X * scalar, vector.Y * scalar, vector.Z * scalar);
}
public static Vector3d operator *(double scalar, Vector3d vector)
{
return vector * scalar;
}
public static Vector3d operator /(Vector3d vector, double scalar)
{
return new Vector3d(vector.X / scalar, vector.Y / scalar, vector.Z / scalar);
}
And for Point3d:
public static Point3d operator +(Point3d point, Vector3d vector)
{
return new Point3d(point.X + vector.X, point.Y + vector.Y, point.Z + vector.Z);
}
public static Point3d operator -(Point3d point, Vector3d vector)
{
return new Point3d(point.X - vector.X, point.Y - vector.Y, point.Z - vector.Z);
}
public static Vector3d operator -(Point3d left, Point3d right)
{
return new Vector3d(right.X - left.X, right.Y - left.Y, right.Z - left.Z);
}
No surprises: the same rules apply, and the same invalid combinations remain forbidden.
Full example: computing a triangle's center
With our operators in place, we can now write geometric algorithms in a readable way. The centroid (center of gravity) of a triangle defined by three points is computed as the average of the three vertices:
\(G = A + \frac{(B - A) + (C - A)}{3}\)
Point2d a = new(0.0, 0.0);
Point2d b = new(6.0, 0.0);
Point2d c = new(3.0, 4.0);
Point2d centroid = a + ((b - a) + (c - a)) / 3.0; // (3, 1.333...)
Each operation has a consistent type:
b - a→Vector2dc - a→Vector2dVector2d + Vector2d→Vector2dVector2d / double→Vector2dPoint2d + Vector2d→Point2d
The compiler checks the entire chain. If you made a mistake — for example writing a + b + c — the code would not compile.
What's next
Our types are now expressive and pleasant to use. In the next article, we'll introduce the unit vector — a dedicated type for directions, which guarantees by construction that its length is always 1.