// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// 2D transformation matrix represented as:
/// [a c tx]
/// [b d ty]
/// [0 0 1 ]
/// where (tx, ty) is translation, and the 2x2 matrix handles rotation/scale/skew
///
pub(all) struct Transform {
a : Double // scale_x * cos(rotation)
b : Double // scale_x * sin(rotation) + skew_y
c : Double // scale_y * -sin(rotation) + skew_x
d : Double // scale_y * cos(rotation)
tx : Double // translation_x
ty : Double // translation_y
}
///|
/// Creates a deep copy of the transform.
///
/// Parameters:
///
/// * `self` : The transform to clone.
///
/// Returns a new `Transform` instance with identical matrix values.
///
pub fn Transform::clone(self : Transform) -> Transform {
{ a: self.a, b: self.b, c: self.c, d: self.d, tx: self.tx, ty: self.ty }
}
///|
/// Create identity transform
///
pub fn Transform::identity() -> Transform {
{ a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx: 0.0, ty: 0.0 }
}
///|
pub impl Default for Transform with fn default() -> Transform {
Transform::identity()
}
///|
/// Create transform from matrix components
///
pub fn Transform::Transform(
a? : Double = 1.0,
b? : Double = 0.0,
c? : Double = 0.0,
d? : Double = 1.0,
tx? : Double = 0.0,
ty? : Double = 0.0,
) -> Transform {
{ a, b, c, d, tx, ty }
}
///|
/// Create translation transform
///
pub fn Transform::from_translation(tx : Double, ty : Double) -> Transform {
{ a: 1.0, b: 0.0, c: 0.0, d: 1.0, tx, ty }
}
///|
/// Create scale transform
///
pub fn Transform::from_scale(sx : Double, sy : Double) -> Transform {
{ a: sx, b: 0.0, c: 0.0, d: sy, tx: 0.0, ty: 0.0 }
}
///|
/// Create rotation transform (degrees)
///
pub fn Transform::from_rotation_deg(deg : Double) -> Transform {
Transform::from_rotation_rad(deg * @cmath.PI / 180.0)
}
///|
/// Create rotation transform (radians)
///
pub fn Transform::from_rotation_rad(rad : Double) -> Transform {
let cos_r = @cmath.cos(rad)
let sin_r = @cmath.sin(rad)
{ a: cos_r, b: sin_r, c: -sin_r, d: cos_r, tx: 0.0, ty: 0.0 }
}
///|
/// Create skew transform
///
pub fn Transform::from_skew(kx : Double, ky : Double) -> Transform {
{ a: 1.0, b: ky, c: kx, d: 1.0, tx: 0.0, ty: 0.0 }
}
///|
/// Multiply two transforms (this * other)
///
pub fn Transform::multiply(self : Transform, other : Transform) -> Transform {
{
a: self.a * other.a + self.c * other.b,
b: self.b * other.a + self.d * other.b,
c: self.a * other.c + self.c * other.d,
d: self.b * other.c + self.d * other.d,
tx: self.a * other.tx + self.c * other.ty + self.tx,
ty: self.b * other.tx + self.d * other.ty + self.ty,
}
}
///|
/// Compose two transforms in left-to-right order (`self * other`).
/// This is equivalent to `multiply`.
pub fn Transform::compose(self : Transform, other : Transform) -> Transform {
self.multiply(other)
}
///|
/// Prepend a transform (`other * self`).
///
/// This means `other` is applied after `self` in point space.
pub fn Transform::prepend(self : Transform, other : Transform) -> Transform {
other * self
}
///|
/// Append a transform (`self * other`).
///
/// This means `other` is applied before `self` in point space.
pub fn Transform::append(self : Transform, other : Transform) -> Transform {
self * other
}
///|
/// Apply transform to a point
///
pub fn Transform::apply_to_point(
self : Transform,
x : Double,
y : Double,
) -> (Double, Double) {
let new_x = self.a * x + self.c * y + self.tx
let new_y = self.b * x + self.d * y + self.ty
(new_x, new_y)
}
///|
pub fn Transform::apply_to_vec2(self : Transform, vec : Vec2) -> Vec2 {
let p = self.apply_to_point(vec.0, vec.1)
Vec2(p.0, p.1)
}
///|
/// Get translation values
///
pub fn Transform::get_translation(self : Transform) -> (Double, Double) {
(self.tx, self.ty)
}
///|
/// Return a new transform with updated translation, keeping linear terms.
pub fn Transform::with_translation(
self : Transform,
tx : Double,
ty : Double,
) -> Transform {
{ ..self, tx, ty }
}
///|
/// Alias of `with_translation`.
pub fn Transform::set_translation(
self : Transform,
tx : Double,
ty : Double,
) -> Transform {
self.with_translation(tx, ty)
}
///|
/// Prepend translation (`T * self`).
pub fn Transform::prepend_translation(
self : Transform,
tx : Double,
ty : Double,
) -> Transform {
Transform::from_translation(tx, ty) * self
}
///|
/// Append translation (`self * T`).
pub fn Transform::append_translation(
self : Transform,
tx : Double,
ty : Double,
) -> Transform {
self * Transform::from_translation(tx, ty)
}
///|
/// Get scale values (ignoring rotation)
///
pub fn Transform::get_scale(self : Transform) -> (Double, Double) {
let sx = (self.a * self.a + self.b * self.b).sqrt()
let sy = (self.c * self.c + self.d * self.d).sqrt()
(sx, sy)
}
///|
/// Prepend scale (`S * self`).
pub fn Transform::prepend_scale(
self : Transform,
sx : Double,
sy : Double,
) -> Transform {
Transform::from_scale(sx, sy) * self
}
///|
/// Append scale (`self * S`).
pub fn Transform::append_scale(
self : Transform,
sx : Double,
sy : Double,
) -> Transform {
self * Transform::from_scale(sx, sy)
}
///|
/// Get rotation angle in radians
///
pub fn Transform::get_rotation_rad(self : Transform) -> Double {
@cmath.atan2(self.b, self.a)
}
///|
/// Prepend rotation (`R * self`).
pub fn Transform::prepend_rotation_rad(
self : Transform,
rad : Double,
) -> Transform {
Transform::from_rotation_rad(rad) * self
}
///|
/// Append rotation (`self * R`).
pub fn Transform::append_rotation_rad(
self : Transform,
rad : Double,
) -> Transform {
self * Transform::from_rotation_rad(rad)
}
///|
/// Prepend rotation in degrees.
pub fn Transform::prepend_rotation_deg(
self : Transform,
deg : Double,
) -> Transform {
self.prepend_rotation_rad(deg * @cmath.PI / 180.0)
}
///|
/// Append rotation in degrees.
pub fn Transform::append_rotation_deg(
self : Transform,
deg : Double,
) -> Transform {
self.append_rotation_rad(deg * @cmath.PI / 180.0)
}
///|
/// Prepend skew (`K * self`).
pub fn Transform::prepend_skew(
self : Transform,
kx : Double,
ky : Double,
) -> Transform {
Transform::from_skew(kx, ky) * self
}
///|
/// Append skew (`self * K`).
pub fn Transform::append_skew(
self : Transform,
kx : Double,
ky : Double,
) -> Transform {
self * Transform::from_skew(kx, ky)
}
///|
/// Calculate inverse transform
///
pub fn Transform::inverse(self : Transform) -> Transform? {
let det = self.a * self.d - self.b * self.c
if det.abs() < 0.0000000001 {
None
} else {
let inv_det = 1.0 / det
Some({
a: self.d * inv_det,
b: -self.b * inv_det,
c: -self.c * inv_det,
d: self.a * inv_det,
tx: (self.c * self.ty - self.d * self.tx) * inv_det,
ty: (self.b * self.tx - self.a * self.ty) * inv_det,
})
}
}
///|
/// Flip transform horizontally
///
pub fn Transform::flip_x(width : Double) -> Transform {
Transform::from_translation(width, 0.0) * Transform::from_scale(-1.0, 1.0)
}
///|
/// Flip transform vertically
///
pub fn Transform::flip_y(height : Double) -> Transform {
Transform::from_translation(0.0, height) * Transform::from_scale(1.0, -1.0)
}
///|
/// Multiply two transforms (matrix multiplication, same as compose)
///
pub impl Mul for Transform with fn mul(self : Transform, other : Transform) -> Transform {
self.multiply(other)
}
// Helper function to check if two doubles are approximately equal
///|
fn approx_eq(a : Double, b : Double, epsilon? : Double = 0.000001) -> Bool {
(a - b).abs() < epsilon
}
// Helper function to check if two transforms are approximately equal
///|
fn transform_approx_eq(
t1 : Transform,
t2 : Transform,
epsilon? : Double = 0.000001,
) -> Bool {
approx_eq(t1.a, t2.a, epsilon~) &&
approx_eq(t1.b, t2.b, epsilon~) &&
approx_eq(t1.c, t2.c, epsilon~) &&
approx_eq(t1.d, t2.d, epsilon~) &&
approx_eq(t1.tx, t2.tx, epsilon~) &&
approx_eq(t1.ty, t2.ty, epsilon~)
}
// Test identity transform
///|
test "Transform::identity creates identity matrix" {
let identity = Transform::identity()
let expected = Transform(a=1.0, b=0.0, c=0.0, d=1.0, tx=0.0, ty=0.0)
assert_true(transform_approx_eq(identity, expected))
}
// Test translation transform
///|
test "Transform::from_translation creates correct translation matrix" {
let translation = Transform::from_translation(10.0, 20.0)
let expected = Transform(a=1.0, b=0.0, c=0.0, d=1.0, tx=10.0, ty=20.0)
assert_true(transform_approx_eq(translation, expected))
}
// Test scale transform
///|
test "Transform::from_scale creates correct scale matrix" {
let scale = Transform::from_scale(2.0, 3.0)
let expected = Transform(a=2.0, b=0.0, c=0.0, d=3.0, tx=0.0, ty=0.0)
assert_true(transform_approx_eq(scale, expected))
}
// Test rotation transform (90 degrees)
///|
test "Transform::from_rotation_deg creates correct rotation matrix" {
let rotation = Transform::from_rotation_deg(90.0)
// 90 degrees: cos(90°) = 0, sin(90°) = 1
let expected = Transform(a=0.0, b=1.0, c=-1.0, d=0.0, tx=0.0, ty=0.0)
assert_true(transform_approx_eq(rotation, expected))
}
// Test rotation transform (π/2 radians)
///|
test "Transform::from_rotation_rad creates correct rotation matrix" {
let rotation = Transform::from_rotation_rad(@cmath.PI / 2.0)
// π/2 radians: cos(π/2) = 0, sin(π/2) = 1
let expected = Transform(a=0.0, b=1.0, c=-1.0, d=0.0, tx=0.0, ty=0.0)
assert_true(transform_approx_eq(rotation, expected))
}
// Test skew transform
///|
test "Transform::from_skew creates correct skew matrix" {
let skew = Transform::from_skew(0.5, 0.3)
let expected = Transform(a=1.0, b=0.3, c=0.5, d=1.0, tx=0.0, ty=0.0)
assert_true(transform_approx_eq(skew, expected))
}
// Test point transformation
///|
test "Transform::apply_to_point transforms points correctly" {
let translation = Transform::from_translation(5.0, 10.0)
let (x, y) = translation.apply_to_point(1.0, 2.0)
assert_true(approx_eq(x, 6.0))
assert_true(approx_eq(y, 12.0))
}
// Test scale point transformation
///|
test "Transform::apply_to_point with scale" {
let scale = Transform::from_scale(2.0, 3.0)
let (x, y) = scale.apply_to_point(4.0, 5.0)
assert_true(approx_eq(x, 8.0))
assert_true(approx_eq(y, 15.0))
}
// Test rotation point transformation (90 degrees)
///|
test "Transform::apply_to_point with rotation" {
let rotation = Transform::from_rotation_deg(90.0)
let (x, y) = rotation.apply_to_point(1.0, 0.0)
assert_true(approx_eq(x, 0.0))
assert_true(approx_eq(y, 1.0))
}
// Test matrix multiplication
///|
test "Transform::multiply composes transformations correctly" {
let translation = Transform::from_translation(10.0, 20.0)
let scale = Transform::from_scale(2.0, 3.0)
let composed = translation.multiply(scale)
// Test that composed transform applies both operations
let (x, y) = composed.apply_to_point(1.0, 1.0)
assert_true(approx_eq(x, 12.0)) // (1 * 2) + 10
assert_true(approx_eq(y, 23.0)) // (1 * 3) + 20
}
// Test operator overloading - multiplication
///|
test "Transform * operator works correctly" {
let translation = Transform::from_translation(5.0, 10.0)
let scale = Transform::from_scale(2.0, 2.0)
let composed1 = translation * scale
let composed2 = translation.multiply(scale)
assert_true(transform_approx_eq(composed1, composed2))
}
// Test explicit prepend/append order
///|
test "Transform::prepend and Transform::append have explicit order" {
let base = Transform::from_scale(2.0, 2.0)
let prepended = base.prepend_translation(10.0, 0.0)
let appended = base.append_translation(10.0, 0.0)
let (x1, y1) = prepended.apply_to_point(1.0, 0.0)
let (x2, y2) = appended.apply_to_point(1.0, 0.0)
assert_true(approx_eq(x1, 12.0))
assert_true(approx_eq(y1, 0.0))
assert_true(approx_eq(x2, 22.0))
assert_true(approx_eq(y2, 0.0))
}
// Test direct translation update API
///|
test "Transform::with_translation updates translation only" {
let base = Transform(a=1.2, b=0.3, c=0.4, d=1.5, tx=9.0, ty=8.0)
let updated = base.with_translation(3.0, 4.0)
assert_true(approx_eq(updated.a, 1.2))
assert_true(approx_eq(updated.b, 0.3))
assert_true(approx_eq(updated.c, 0.4))
assert_true(approx_eq(updated.d, 1.5))
assert_true(approx_eq(updated.tx, 3.0))
assert_true(approx_eq(updated.ty, 4.0))
}
// Test get/set translation
///|
test "Transform get/set translation works correctly" {
let transform = Transform::from_scale(2.0, 3.0)
let (tx, ty) = transform.get_translation()
assert_true(approx_eq(tx, 0.0))
assert_true(approx_eq(ty, 0.0))
let new_transform = transform.prepend_translation(10.0, 20.0)
let (new_tx, new_ty) = new_transform.get_translation()
assert_true(approx_eq(new_tx, 10.0))
assert_true(approx_eq(new_ty, 20.0))
// Ensure scale is preserved
let (sx, sy) = new_transform.get_scale()
assert_true(approx_eq(sx, 2.0))
assert_true(approx_eq(sy, 3.0))
}
// Test get/set scale
///|
test "Transform get/set scale works correctly" {
let transform = Transform::from_translation(10.0, 20.0)
let (sx, sy) = transform.get_scale()
assert_true(approx_eq(sx, 1.0))
assert_true(approx_eq(sy, 1.0))
let new_transform = transform.prepend_scale(2.0, 3.0)
let (new_sx, new_sy) = new_transform.get_scale()
assert_true(approx_eq(new_sx, 2.0))
assert_true(approx_eq(new_sy, 3.0))
let (tx, ty) = new_transform.get_translation()
assert_true(approx_eq(tx, 20.0))
assert_true(approx_eq(ty, 60.0))
}
// Test get/set rotation
///|
test "Transform get/set rotation works correctly" {
let transform = Transform::from_scale(2.0, 3.0)
let rotation = transform.get_rotation_rad()
assert_true(approx_eq(rotation, 0.0))
let new_transform = transform.prepend_rotation_rad(@cmath.PI / 4.0) // 45 degrees
let new_rotation = new_transform.get_rotation_rad()
assert_true(approx_eq(new_rotation, @cmath.PI / 4.0))
// Ensure scale is preserved (approximately)
let (sx, sy) = new_transform.get_scale()
assert_true(approx_eq(sx, 2.0))
assert_true(approx_eq(sy, 3.0))
}
// Test inverse transform
///|
test "Transform::inverse calculates correct inverse" {
let translation = Transform::from_translation(10.0, 20.0)
let inverse = translation.inverse()
match inverse {
Some(inv) => {
let composed = translation * inv
let identity = Transform::identity()
assert_true(transform_approx_eq(composed, identity))
}
None => abort("Translation transform should be invertible")
}
}
// Test inverse of rotation
///|
test "Transform::inverse with rotation" {
let rotation = Transform::from_rotation_deg(45.0)
let inverse = rotation.inverse()
match inverse {
Some(inv) => {
let composed = rotation * inv
let identity = Transform::identity()
assert_true(transform_approx_eq(composed, identity))
}
None => abort("Rotation transform should be invertible")
}
}
// Test inverse of non-invertible matrix
///|
test "Transform::inverse returns None for non-invertible matrix" {
// Create a matrix with zero determinant (not invertible)
let non_invertible = Transform(a=0.0, b=0.0, c=0.0, d=0.0, tx=0.0, ty=0.0)
let inverse = non_invertible.inverse()
match inverse {
Some(_) => abort("Non-invertible matrix should return None")
None => assert_true(true)
}
}
// Test complex composition
///|
test "Complex transform composition" {
let translation = Transform::from_translation(10.0, 5.0)
let rotation = Transform::from_rotation_deg(90.0)
let scale = Transform::from_scale(2.0, 3.0)
// Compose: translate, then rotate, then scale
let composed = translation * rotation * scale
// Test with a point
let (x, y) = composed.apply_to_point(1.0, 0.0)
// Manual calculation:
// 1. Scale 2x, 3x (1,0) -> (2,0)
// 2. Rotate 90° (2,0) -> (0,2)
// 3. Translate (0,2) -> (10,7)
assert_true(approx_eq(x, 10.0))
assert_true(approx_eq(y, 7.0))
}
// Test flip_x functionality
///|
test "Transform::flip_x creates correct horizontal flip" {
let flip = Transform::flip_x(100.0)
// Test a point transformation
let (x, y) = flip.apply_to_point(10.0, 20.0)
assert_true(approx_eq(x, 90.0)) // 100 - 10 = 90
assert_true(approx_eq(y, 20.0)) // y unchanged
}
// Test flip_y functionality
///|
test "Transform::flip_y creates correct vertical flip" {
let flip = Transform::flip_y(200.0)
// Test a point transformation
let (x, y) = flip.apply_to_point(30.0, 40.0)
assert_true(approx_eq(x, 30.0)) // x unchanged
assert_true(approx_eq(y, 160.0)) // 200 - 40 = 160
}