Editor/Effigy/Sketch/Constraints.cs
using System;

namespace Effigy;

// The constraint set. Every one of these is a residual that is zero when the rule holds, plus the
// derivative of that residual against each point it touches.
//
// THE DERIVATIVES ARE THE WHOLE FILE. Get a residual wrong and the solver converges confidently to
// the wrong shape; get a derivative wrong and it converges slowly, or oscillates, or wanders off —
// and none of that looks like a signed error, it looks like "the solver is flaky". They are checked
// two ways in ConstraintTests: against a central finite difference of the residual itself, which
// catches a wrong sign or a swapped term, and by solving a sketch whose answer is known in closed
// form. Add a constraint here and add both.
//
// Sign convention throughout: residuals are (what it is) − (what it should be), and points are
// addressed by index into Sketch.Points.

/// <summary>Two points occupy the same location. Two rows, because x and y are independently
/// wrong.</summary>
public sealed class CoincidentConstraint : IConstraint
{
	public readonly int A, B;

	public CoincidentConstraint( int a, int b )
	{
		A = a;
		B = b;
	}

	public int ResidualCount => 2;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var pa = points[A];
		var pb = points[B];

		output[0] = new ConstraintResult( (double)pa.x - pb.x, new[] { (A, 1.0, 0.0), (B, -1.0, 0.0) } );
		output[1] = new ConstraintResult( (double)pa.y - pb.y, new[] { (A, 0.0, 1.0), (B, 0.0, -1.0) } );
	}
}

/// <summary>The distance between two points equals Value. This is the dimension a user types.</summary>
public sealed class DistanceConstraint : IConstraint
{
	public readonly int A, B;
	public readonly double Value;

	public DistanceConstraint( int a, int b, double value )
	{
		A = a;
		B = b;
		Value = value;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dx = (double)points[A].x - points[B].x;
		var dy = (double)points[A].y - points[B].y;
		var d = Math.Sqrt( dx * dx + dy * dy );

		// Coincident points have no direction to separate along and the true derivative is
		// undefined. Picking +X keeps the Jacobian finite and the next step breaks the tie; leaving
		// it as 0/0 would put a NaN into H and take the whole solve with it.
		if ( d < 1e-12 )
		{
			output[0] = new ConstraintResult( -Value, new[] { (A, 1.0, 0.0), (B, -1.0, 0.0) } );
			return;
		}

		var inv = 1.0 / d;

		output[0] = new ConstraintResult( d - Value, new[]
		{
			(A,  dx * inv,  dy * inv),
			(B, -dx * inv, -dy * inv)
		} );
	}
}

/// <summary>The segment between two points is horizontal: Δy = 0.</summary>
public sealed class HorizontalConstraint : IConstraint
{
	public readonly int Start, End;

	public HorizontalConstraint( int start, int end )
	{
		Start = start;
		End = end;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dy = (double)points[Start].y - points[End].y;

		output[0] = new ConstraintResult( dy, new[] { (Start, 0.0, 1.0), (End, 0.0, -1.0) } );
	}
}

/// <summary>The segment between two points is vertical: Δx = 0.</summary>
public sealed class VerticalConstraint : IConstraint
{
	public readonly int Start, End;

	public VerticalConstraint( int start, int end )
	{
		Start = start;
		End = end;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dx = (double)points[Start].x - points[End].x;

		output[0] = new ConstraintResult( dx, new[] { (Start, 1.0, 0.0), (End, -1.0, 0.0) } );
	}
}

/// <summary>Two segments are the same length, without saying what that length is.</summary>
public sealed class EqualLengthConstraint : IConstraint
{
	public readonly int A0, A1, B0, B1;

	public EqualLengthConstraint( int a0, int a1, int b0, int b1 )
	{
		A0 = a0;
		A1 = a1;
		B0 = b0;
		B1 = b1;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var (da, dxa, dya) = Delta( points[A0], points[A1] );
		var (db, dxb, dyb) = Delta( points[B0], points[B1] );

		// A degenerate segment has no length gradient. Zeroing its columns leaves the other
		// segment free to move toward it, which is the useful half of the answer.
		var invA = da > 1e-12 ? 1.0 / da : 0.0;
		var invB = db > 1e-12 ? 1.0 / db : 0.0;

		output[0] = new ConstraintResult( da - db, new[]
		{
			(A0,  dxa * invA,  dya * invA),
			(A1, -dxa * invA, -dya * invA),
			(B0, -dxb * invB, -dyb * invB),
			(B1,  dxb * invB,  dyb * invB)
		} );
	}

	static (double Length, double Dx, double Dy) Delta( Vec2 p, Vec2 q )
	{
		var dx = (double)p.x - q.x;
		var dy = (double)p.y - q.y;

		return (Math.Sqrt( dx * dx + dy * dy ), dx, dy);
	}
}

/// <summary>
/// Two segments are parallel, as the 2D cross product of their directions being zero.
///
/// Not the angle between them: an angle needs an atan2 and carries a branch cut, and the cross
/// product is smooth everywhere and zero at exactly the states wanted. It is also sign-blind, so
/// antiparallel counts as parallel — which is what a CAD parallel constraint means.
/// </summary>
public sealed class ParallelConstraint : IConstraint
{
	public readonly int A0, A1, B0, B1;

	public ParallelConstraint( int a0, int a1, int b0, int b1 )
	{
		A0 = a0;
		A1 = a1;
		B0 = b0;
		B1 = b1;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var ux = (double)points[A1].x - points[A0].x;
		var uy = (double)points[A1].y - points[A0].y;
		var vx = (double)points[B1].x - points[B0].x;
		var vy = (double)points[B1].y - points[B0].y;

		output[0] = new ConstraintResult( ux * vy - uy * vx, new[]
		{
			(A0, -vy,  vx),
			(A1,  vy, -vx),
			(B0,  uy, -ux),
			(B1, -uy,  ux)
		} );
	}
}

/// <summary>Two segments meet at a right angle, as the dot product of their directions being
/// zero.</summary>
public sealed class PerpendicularConstraint : IConstraint
{
	public readonly int A0, A1, B0, B1;

	public PerpendicularConstraint( int a0, int a1, int b0, int b1 )
	{
		A0 = a0;
		A1 = a1;
		B0 = b0;
		B1 = b1;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var ux = (double)points[A1].x - points[A0].x;
		var uy = (double)points[A1].y - points[A0].y;
		var vx = (double)points[B1].x - points[B0].x;
		var vy = (double)points[B1].y - points[B0].y;

		output[0] = new ConstraintResult( ux * vx + uy * vy, new[]
		{
			(A0, -vx, -vy),
			(A1,  vx,  vy),
			(B0, -ux, -uy),
			(B1,  ux,  uy)
		} );
	}
}

/// <summary>
/// Two segments meet at a fixed angle.
///
/// The residual is |u||v| sin(φ − θ), written as cross·cos θ − dot·sin θ so that neither an atan2
/// nor a normalisation appears in it. That matters more than it looks: an angle computed with atan2
/// has a branch cut, and a residual that jumps by 2π somewhere in its domain will send a solver off
/// in the wrong direction the moment a line crosses it. This form is smooth everywhere and zero at
/// exactly the states wanted.
///
/// Parallel and Perpendicular are this at 0 and 90 degrees. They stay separate types because they
/// are what a user asks for, and because neither needs a value stored alongside it.
/// </summary>
public sealed class AngleConstraint : IConstraint
{
	public readonly int A0, A1, B0, B1;
	public readonly double Degrees;

	public AngleConstraint( int a0, int a1, int b0, int b1, double degrees )
	{
		A0 = a0;
		A1 = a1;
		B0 = b0;
		B1 = b1;
		Degrees = degrees;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var ux = (double)points[A1].x - points[A0].x;
		var uy = (double)points[A1].y - points[A0].y;
		var vx = (double)points[B1].x - points[B0].x;
		var vy = (double)points[B1].y - points[B0].y;

		var radians = Degrees * Math.PI / 180.0;
		var cos = Math.Cos( radians );
		var sin = Math.Sin( radians );

		var cross = ux * vy - uy * vx;
		var dot = ux * vx + uy * vy;

		// d(residual)/du and /dv, from which the four point derivatives follow by u = A1 - A0.
		var dUx = vy * cos - vx * sin;
		var dUy = -vx * cos - vy * sin;
		var dVx = -uy * cos - ux * sin;
		var dVy = ux * cos - uy * sin;

		output[0] = new ConstraintResult( cross * cos - dot * sin, new[]
		{
			(A0, -dUx, -dUy),
			(A1,  dUx,  dUy),
			(B0, -dVx, -dVy),
			(B1,  dVx,  dVy)
		} );
	}
}

/// <summary>
/// A point lies on the infinite line through two others.
///
/// The residual is the cross product of the line's direction with the vector to the point, which is
/// twice the area of the triangle they make — zero exactly when they are collinear. Not the
/// perpendicular DISTANCE, which would need a division by the line's length and blow up as the two
/// defining points approach each other; the unnormalised form is smooth everywhere and vanishes at
/// the same states.
/// </summary>
public sealed class PointOnLineConstraint : IConstraint
{
	public readonly int Point, A, B;

	public PointOnLineConstraint( int point, int a, int b )
	{
		Point = point;
		A = a;
		B = b;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dx = (double)points[B].x - points[A].x;
		var dy = (double)points[B].y - points[A].y;
		var wx = (double)points[Point].x - points[A].x;
		var wy = (double)points[Point].y - points[A].y;

		output[0] = new ConstraintResult( dx * wy - dy * wx, new[]
		{
			(Point, -dy, dx),
			(A, dy - wy, wx - dx),
			(B, wy, -wx)
		} );
	}
}

/// <summary>
/// Two points mirror each other across the line through two others.
///
/// Two rows, because symmetry is two independent statements and collapsing them into one distance
/// would let the solver satisfy it by putting both points in the same place. Their midpoint has to
/// sit ON the line, and the segment between them has to cross it at a right angle.
/// </summary>
public sealed class SymmetricConstraint : IConstraint
{
	public readonly int P, Q, A, B;

	public SymmetricConstraint( int p, int q, int a, int b )
	{
		P = p;
		Q = q;
		A = a;
		B = b;
	}

	public int ResidualCount => 2;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dx = (double)points[B].x - points[A].x;
		var dy = (double)points[B].y - points[A].y;

		// Midpoint of P and Q, relative to A.
		var mx = ((double)points[P].x + points[Q].x) * 0.5 - points[A].x;
		var my = ((double)points[P].y + points[Q].y) * 0.5 - points[A].y;

		// Row one: the midpoint is on the line. Each of P and Q moves it by a half.
		output[0] = new ConstraintResult( dx * my - dy * mx, new[]
		{
			(P, -dy * 0.5, dx * 0.5),
			(Q, -dy * 0.5, dx * 0.5),
			(A, dy - my, mx - dx),
			(B, my, -mx)
		} );

		// Row two: PQ is perpendicular to the line.
		var qx = (double)points[Q].x - points[P].x;
		var qy = (double)points[Q].y - points[P].y;

		output[1] = new ConstraintResult( dx * qx + dy * qy, new[]
		{
			(P, -dx, -dy),
			(Q, dx, dy),
			(A, -qx, -qy),
			(B, qx, qy)
		} );
	}
}

/// <summary>
/// A point sits exactly half way between two others. Two rows, because x and y are independently
/// wrong — the same reason Coincident is two.
///
/// Not expressible as two equal distances: that would also be satisfied by the point sitting
/// anywhere on the perpendicular bisector, which is a circle's worth of wrong answers.
/// </summary>
public sealed class MidpointConstraint : IConstraint
{
	public readonly int P, A, B;

	public MidpointConstraint( int p, int a, int b )
	{
		P = p;
		A = a;
		B = b;
	}

	public int ResidualCount => 2;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var mx = ((double)points[A].x + points[B].x) * 0.5;
		var my = ((double)points[A].y + points[B].y) * 0.5;

		output[0] = new ConstraintResult( (double)points[P].x - mx,
			new[] { (P, 1.0, 0.0), (A, -0.5, 0.0), (B, -0.5, 0.0) } );

		output[1] = new ConstraintResult( (double)points[P].y - my,
			new[] { (P, 0.0, 1.0), (A, 0.0, -0.5), (B, 0.0, -0.5) } );
	}
}

/// <summary>
/// A point is nailed to an absolute coordinate — Onshape's "fix".
///
/// THE SOLVER'S PIN IS NOT THIS. SketchSolver.Solve takes a single pinnedPoint and removes its
/// columns from the Jacobian entirely, which is how the sketch gets an absolute frame at all; it
/// can only ever be one point and it is chosen by the caller, not by the user. This is the
/// user-facing version and there can be as many as you like. It works the ordinary way, as two
/// residuals the solver drives to zero, so a fix that fights a dimension shows up honestly as a
/// sketch that will not converge rather than as a silently ignored rule.
/// </summary>
public sealed class FixedConstraint : IConstraint
{
	public readonly int P;
	public readonly double X, Y;

	public FixedConstraint( int p, double x, double y )
	{
		P = p;
		X = x;
		Y = y;
	}

	public int ResidualCount => 2;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		output[0] = new ConstraintResult( (double)points[P].x - X, new[] { (P, 1.0, 0.0) } );
		output[1] = new ConstraintResult( (double)points[P].y - Y, new[] { (P, 0.0, 1.0) } );
	}
}

/// <summary>
/// A line is tangent to a circle or an arc: the distance from the centre to the infinite line
/// equals the radius.
///
/// The circle is given as a centre and a point on its rim rather than as a stored radius, because
/// that is the only form the solver can see. Every unknown here is a point coordinate — a radius
/// held in a float field is invisible to the Jacobian and could not be driven by anything. For a
/// SketchArc the rim point is its Start, which is exactly how SketchArc already defines its radius.
///
/// WRITTEN IN LENGTH-SQUARED, AND THE SCALING IS THE REASON. The obvious residual is
/// cross(d, w)^2 - r^2*|d|^2, which clears the division and is quartic in the coordinates. It is
/// smooth and it is correct, and it is badly scaled: the solver's convergence test is an absolute
/// 1e-6 on the residual norm, so a quartic residual reaching 1e-6 can still be a visibly untangent
/// line. Dividing through by |d|^2 makes both terms an area, so 1e-6 of residual is about 5e-7 of
/// radius on a unit-ish sketch, which is the accuracy the number implies.
///
/// Sign-blind on purpose: the line can arrive at tangency from either side, and forcing a side
/// would mean choosing one at build time from the current configuration and having the constraint
/// mean something different depending on when it was added.
/// </summary>
public sealed class TangentLineArcConstraint : IConstraint
{
	public readonly int A0, A1, Center, Rim;

	public TangentLineArcConstraint( int a0, int a1, int center, int rim )
	{
		A0 = a0;
		A1 = a1;
		Center = center;
		Rim = rim;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dx = (double)points[A1].x - points[A0].x;
		var dy = (double)points[A1].y - points[A0].y;
		var wx = (double)points[Center].x - points[A0].x;
		var wy = (double)points[Center].y - points[A0].y;

		var rx = (double)points[Center].x - points[Rim].x;
		var ry = (double)points[Center].y - points[Rim].y;
		var r2 = rx * rx + ry * ry;

		var d2 = dx * dx + dy * dy;

		// A zero-length line has no direction and no distance-to-line. Reporting the radius as the
		// error with a zero gradient on the line leaves the circle free to shrink toward it, which
		// is the only sensible half of the answer, and keeps a NaN out of H.
		if ( d2 < 1e-18 )
		{
			output[0] = new ConstraintResult( -r2, new[]
			{
				(Center, -2.0 * rx, -2.0 * ry),
				(Rim, 2.0 * rx, 2.0 * ry)
			} );

			return;
		}

		var k = dx * wy - dy * wx;
		var q = k * k / d2;

		var a = 2.0 * k / d2;
		var b = q / d2;

		output[0] = new ConstraintResult( q - r2, new[]
		{
			(A0, a * (dy - wy) + 2.0 * b * dx, a * (wx - dx) + 2.0 * b * dy),
			(A1, a * wy - 2.0 * b * dx, -a * wx - 2.0 * b * dy),
			(Center, -a * dy - 2.0 * rx, a * dx - 2.0 * ry),
			(Rim, 2.0 * rx, 2.0 * ry)
		} );
	}
}

/// <summary>
/// Two circles or arcs are tangent to each other: centre distance equals the sum of the radii
/// (touching outside) or the difference (one nestled inside the other).
///
/// Which of the two is a stored choice rather than something inferred from the current positions.
/// Inferring would make the rule mean whichever one happened to be closer when it was added, and
/// then silently flip meaning the first time a drag carried the circles past each other.
///
/// In plain lengths rather than squares — unlike the line case there is no division to clear here,
/// so the residual is already a distance and 1e-6 of residual is 1e-6 of gap.
/// </summary>
public sealed class TangentArcArcConstraint : IConstraint
{
	public readonly int CenterA, RimA, CenterB, RimB;

	/// <summary>True when one circle sits inside the other and they touch at a single point.</summary>
	public readonly bool Internal;

	public TangentArcArcConstraint( int centerA, int rimA, int centerB, int rimB, bool internalTangency = false )
	{
		CenterA = centerA;
		RimA = rimA;
		CenterB = centerB;
		RimB = rimB;
		Internal = internalTangency;
	}

	public int ResidualCount => 1;

	public void Evaluate( ReadOnlySpan<Vec2> points, Span<ConstraintResult> output )
	{
		var dx = (double)points[CenterA].x - points[CenterB].x;
		var dy = (double)points[CenterA].y - points[CenterB].y;
		var l = Math.Sqrt( dx * dx + dy * dy );

		var ax = (double)points[CenterA].x - points[RimA].x;
		var ay = (double)points[CenterA].y - points[RimA].y;
		var ra = Math.Sqrt( ax * ax + ay * ay );

		var bx = (double)points[CenterB].x - points[RimB].x;
		var by = (double)points[CenterB].y - points[RimB].y;
		var rb = Math.Sqrt( bx * bx + by * by );

		// Every direction below is a unit vector that does not exist when its length is zero.
		// Zeroing the gradient there leaves the other terms to do the work rather than poisoning
		// the whole step with a NaN, the same tactic DistanceConstraint uses.
		var lx = l > 1e-12 ? dx / l : 0.0;
		var ly = l > 1e-12 ? dy / l : 0.0;
		var uax = ra > 1e-12 ? ax / ra : 0.0;
		var uay = ra > 1e-12 ? ay / ra : 0.0;
		var ubx = rb > 1e-12 ? bx / rb : 0.0;
		var uby = rb > 1e-12 ? by / rb : 0.0;

		if ( !Internal )
		{
			output[0] = new ConstraintResult( l - ra - rb, new[]
			{
				(CenterA, lx - uax, ly - uay),
				(CenterB, -lx - ubx, -ly - uby),
				(RimA, uax, uay),
				(RimB, ubx, uby)
			} );

			return;
		}

		// |ra - rb| is not differentiable where the radii are equal, and that state is a real one:
		// two equal circles are internally tangent exactly when they are the same circle. The sign
		// is taken from the current configuration, which is the standard handling and is stable
		// everywhere except that degenerate point.
		var s = ra >= rb ? 1.0 : -1.0;

		output[0] = new ConstraintResult( l - Math.Abs( ra - rb ), new[]
		{
			(CenterA, lx - s * uax, ly - s * uay),
			(CenterB, -lx + s * ubx, -ly + s * uby),
			(RimA, s * uax, s * uay),
			(RimB, -s * ubx, -s * uby)
		} );
	}
}