using EnvelopeRenderer.Cli.Render; namespace EnvelopeRenderer.Cli.Tests; public class RotatedTextAnchorCalculatorTests { [Fact] public void ComputeAnchor_ZeroAngle_ReturnsOriginalAnchorUnchanged() { var (x, y) = RotatedTextAnchorCalculator.ComputeAnchor( x: 120, y: 240, width: 50, ascent: 10, descent: -3, angleDegrees: 0); Assert.Equal(120, x, precision: 9); Assert.Equal(240, y, precision: 9); } [Fact] public void ComputeAnchor_180Degrees_ReflectsAnchorThroughBoundingBoxCenter() { // A 180-degree rotation about a fixed center C maps any point P to 2C - P. Applying that // to the anchor itself: the corrected anchor should be exactly the bbox center reflected // through itself minus... concretely: center = (x + width/2, y + (ascent+descent)/2); // for a 180-degree rotation, anchor' = 2*center - (x, y). const double x = 100; const double y = 100; const double width = 40; const double ascent = 20; const double descent = -4; var centerX = x + (width / 2.0); var centerY = y + ((ascent + descent) / 2.0); var (anchorX, anchorY) = RotatedTextAnchorCalculator.ComputeAnchor(x, y, width, ascent, descent, 180); Assert.Equal((2 * centerX) - x, anchorX, precision: 6); Assert.Equal((2 * centerY) - y, anchorY, precision: 6); } [Fact] public void ComputeAnchor_90Degrees_MatchesHandDerivedCounterclockwiseFormula() { // Confirmed empirically against the real Debenu DLL (see RotatedTextAnchorCalculator's // class remarks): positive angle is counterclockwise in PDF's Y-up space, i.e. the // standard math rotation matrix applied directly with no sign flip. const double x = 0; const double y = 0; const double width = 10; // centerLocal = (5, 5) const double ascent = 12; const double descent = -2; // (ascent+descent)/2 = 5 var (anchorX, anchorY) = RotatedTextAnchorCalculator.ComputeAnchor(x, y, width, ascent, descent, 90); // R(90) * (5,5) = (5*cos90 - 5*sin90, 5*sin90 + 5*cos90) = (-5, 5). // anchor' = (x,y) + centerLocal - R(90)*centerLocal = (0,0) + (5,5) - (-5,5) = (10, 0). Assert.Equal(10, anchorX, precision: 6); Assert.Equal(0, anchorY, precision: 6); } [Fact] public void ComputeAnchor_NegativeAngle_RotatesTheOppositeDirection() { var (positiveX, positiveY) = RotatedTextAnchorCalculator.ComputeAnchor(50, 50, 30, 15, -3, 40); var (negativeX, negativeY) = RotatedTextAnchorCalculator.ComputeAnchor(50, 50, 30, 15, -3, -40); Assert.NotEqual(positiveX, negativeX, precision: 3); Assert.NotEqual(positiveY, negativeY, precision: 3); } [Theory] [InlineData(30)] [InlineData(90)] [InlineData(180)] [InlineData(-45)] [InlineData(270)] public void ComputeAnchor_AnyAngle_PreservesTheUnrotatedBoundingBoxCenter(double angle) { // The whole point of this calculator: regardless of angle, the resulting anchor, once // Debenu rotates its local bbox center around it by `angle`, lands back on the *original* // (unrotated) bbox center. Verify that directly by applying the same rotation formula // DrawRotatedText uses (confirmed CCW-positive) to the anchor's own local center offset. const double x = 77; const double y = 33; const double width = 64; const double ascent = 18; const double descent = -6; var centerLocalX = width / 2.0; var centerLocalY = (ascent + descent) / 2.0; var expectedCenterX = x + centerLocalX; var expectedCenterY = y + centerLocalY; var (anchorX, anchorY) = RotatedTextAnchorCalculator.ComputeAnchor(x, y, width, ascent, descent, angle); var radians = angle * Math.PI / 180.0; var cos = Math.Cos(radians); var sin = Math.Sin(radians); var rotatedCenterX = anchorX + ((centerLocalX * cos) - (centerLocalY * sin)); var rotatedCenterY = anchorY + ((centerLocalX * sin) + (centerLocalY * cos)); Assert.Equal(expectedCenterX, rotatedCenterX, precision: 6); Assert.Equal(expectedCenterY, rotatedCenterY, precision: 6); } }