namespace Age.Engine.Model; /// A projected row-vector affine transform: x'=x*XX+y*YX+TX, y'=x*XY+y*YY+TY. public readonly record struct Affine2D(double XX, double XY, double YX, double YY, double TX, double TY) { public (double X, double Y) Apply(double x, double y) => (x * XX + y * YX + TX, x * XY + y * YY + TY); public Affine2D FromLocalOrigin(double worldX, double worldY) { var p = Apply(worldX, worldY); return new(XX, XY, YX, YY, p.X, p.Y); } public bool TryInverse(out Affine2D inverse) { double det = XX * YY - XY * YX; if (System.Math.Abs(det) < 1e-12) { inverse = default; return false; } double xx = YY / det, xy = -XY / det, yx = -YX / det, yy = XX / det; inverse = new(xx, xy, yx, yy, -(TX * xx + TY * yx), -(TX * xy + TY * yy)); return true; } } /// Exact 2D projection of AGE's row-vector retained-object matrix. Native call order is anchored /// scale, one-shot axis-angle rotation, translation, then separately anchored cyclic rotation. The adjacent /// anchor translations cancel, yielding T(-a)*S*R1*T*Rcycle*T(+a). Z is projected away only afterward. public static class Transform2DMath { public static Affine2D Build(TransformState t, RotationCycleState cycle = default) { double[] m = Identity(); m = Mul(m, Translation(-t.AnchorX, -t.AnchorY, -t.AnchorZ)); m = Mul(m, Scale(t.ScaleX, t.ScaleY, t.ScaleZ)); m = Mul(m, AxisAngle(t.RotationAxisX, t.RotationAxisY, t.RotationAxisZ, t.RotationAngleDegrees)); m = Mul(m, Translation(t.TranslateX, t.TranslateY, t.TranslateZ)); if (cycle.Enabled) m = Mul(m, AxisAngle(cycle.AxisX, cycle.AxisY, cycle.AxisZ, cycle.AngleDegrees)); m = Mul(m, Translation(t.AnchorX, t.AnchorY, t.AnchorZ)); return new(m[0], m[1], m[4], m[5], m[12], m[13]); } public static (double X, double Y) Apply(double x, double y, TransformState transform, RotationCycleState cycle = default) => Build(transform, cycle).Apply(x, y); private static double[] Identity() => new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1 }; private static double[] Scale(double x, double y, double z) => new double[] { x,0,0,0, 0,y,0,0, 0,0,z,0, 0,0,0,1 }; private static double[] Translation(double x, double y, double z) => new double[] { 1,0,0,0, 0,1,0,0, 0,0,1,0, x,y,z,1 }; private static double[] AxisAngle(double x, double y, double z, double degrees) { double len = System.Math.Sqrt(x*x + y*y + z*z); if (len < 1e-12 || System.Math.Abs(degrees) < 1e-12) return Identity(); x /= len; y /= len; z /= len; double r = degrees * System.Math.PI / 180.0, c = System.Math.Cos(r), s = System.Math.Sin(r), q = 1-c; return new double[] { x*x*q+c, x*y*q+z*s, x*z*q-y*s, 0, x*y*q-z*s, y*y*q+c, y*z*q+x*s, 0, x*z*q+y*s, y*z*q-x*s, z*z*q+c, 0, 0,0,0,1 }; } private static double[] Mul(double[] a, double[] b) { var o = new double[16]; for (int row=0; row<4; row++) for (int col=0; col<4; col++) for (int k=0; k<4; k++) o[row*4+col] += a[row*4+k] * b[k*4+col]; return o; } }