mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-26 03:16:53 -06:00
62 lines
2.4 KiB
Rust
62 lines
2.4 KiB
Rust
//! Decomposing and composing the 2×3 node transforms the design panel shows as X, Y and rotation.
|
||
|
||
use kurbo::{Affine, Point, Vec2};
|
||
|
||
/// Rotation in degrees (counter-clockwise on screen, as Figma's rotation field shows it) of `xf`.
|
||
pub fn rotation_degrees(xf: Affine) -> f64 {
|
||
let [a, b, ..] = xf.as_coeffs();
|
||
// y-down: positive Figma rotation turns counter-clockwise on screen, i.e. a negative math angle.
|
||
let deg = -b.atan2(a).to_degrees();
|
||
let deg = if deg.abs() < 1e-9 { 0.0 } else { deg };
|
||
if deg <= -180.0 { deg + 360.0 } else { deg }
|
||
}
|
||
|
||
/// The translation part of `xf`.
|
||
pub fn translation(xf: Affine) -> Point {
|
||
let c = xf.as_coeffs();
|
||
Point::new(c[4], c[5])
|
||
}
|
||
|
||
/// Replace the translation of `xf`.
|
||
pub fn with_translation(xf: Affine, p: Point) -> Affine {
|
||
let c = xf.as_coeffs();
|
||
Affine::new([c[0], c[1], c[2], c[3], p.x, p.y])
|
||
}
|
||
|
||
/// Whether the linear part is a mirror (negative determinant).
|
||
pub fn is_flipped(xf: Affine) -> bool {
|
||
xf.determinant() < 0.0
|
||
}
|
||
|
||
/// A transform with the given translation and rotation (degrees, Figma convention).
|
||
pub fn from_position_rotation(x: f64, y: f64, degrees: f64) -> Affine {
|
||
Affine::translate(Vec2::new(x, y)) * Affine::rotate(-degrees.to_radians())
|
||
}
|
||
|
||
/// Change the rotation of `xf` to `degrees` about the centre of a `w`×`h` box in its local space,
|
||
/// keeping that centre in place and keeping any flip.
|
||
pub fn set_rotation_about_center(xf: Affine, w: f64, h: f64, degrees: f64) -> Affine {
|
||
let center_local = Point::new(w / 2.0, h / 2.0);
|
||
let center = xf * center_local;
|
||
let flip = if is_flipped(xf) { Affine::FLIP_X } else { Affine::IDENTITY };
|
||
let rot = Affine::rotate(-degrees.to_radians());
|
||
let lin = rot * flip;
|
||
// Solve translation so that lin * center_local + t = center.
|
||
let mapped = lin * center_local;
|
||
let t = center - mapped;
|
||
let c = lin.as_coeffs();
|
||
Affine::new([c[0], c[1], c[2], c[3], t.x, t.y])
|
||
}
|
||
|
||
/// Rotate `xf` by `degrees` (Figma convention) about the point `pivot` (parent space).
|
||
pub fn rotate_about(xf: Affine, pivot: Point, degrees: f64) -> Affine {
|
||
Affine::translate(pivot.to_vec2()) * Affine::rotate(-degrees.to_radians()) * Affine::translate(-pivot.to_vec2()) * xf
|
||
}
|
||
|
||
/// Round a value to `digits` decimals for display (avoids `-0` and float noise).
|
||
pub fn round_to(v: f64, digits: i32) -> f64 {
|
||
let f = 10f64.powi(digits);
|
||
let r = (v * f).round() / f;
|
||
if r == 0.0 { 0.0 } else { r }
|
||
}
|