pub const DEFAULT_WORKSPACE_BG: [f32; 4] = [0.11, 0.11, 0.11, 1.0];
#[derive(Copy, Clone, Debug)]
pub struct ViewParams {
pub pan_x: f32,
pub pan_y: f32,
pub zoom: f32,
pub rotation: f32,
pub mirror_h: bool,
pub screen_w: f32,
pub screen_h: f32,
}
impl Default for ViewParams {
fn default() -> Self {
ViewParams {
pan_x: 0.0,
pan_y: 0.0,
zoom: 1.0,
rotation: 0.0,
mirror_h: false,
screen_w: 1.0,
screen_h: 1.0,
}
}
}
#[repr(C)]
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct ViewTransform {
pub matrix: [[f32; 4]; 3],
pub bg: [f32; 4],
pub flags: [f32; 4],
}
impl ViewTransform {
pub fn identity() -> Self {
ViewTransform {
matrix: [
[1.0, 0.0, 1.0, 0.0],
[0.0, 1.0, 1.0, 0.0],
[0.0, 0.0, 1.0, 0.0],
],
bg: DEFAULT_WORKSPACE_BG,
flags: [0.0; 4],
}
}
pub fn from_pan_zoom_rotate(
pan_x: f32,
pan_y: f32,
zoom: f32,
rotation: f32, mirror_h: bool,
screen_w: f32,
screen_h: f32,
canvas_w: f32,
canvas_h: f32,
) -> Self {
let cos_r = rotation.cos();
let sin_r = rotation.sin();
let inv_zoom = 1.0 / zoom;
let cx = canvas_w / 2.0;
let cy = canvas_h / 2.0;
let sx = screen_w / 2.0 + pan_x;
let sy = screen_h / 2.0 + pan_y;
let mut m00 = cos_r * inv_zoom;
let m01 = sin_r * inv_zoom;
let mut m10 = -sin_r * inv_zoom;
let m11 = cos_r * inv_zoom;
let mut tx = cx - m00 * sx - m10 * sy;
let ty = cy - m01 * sx - m11 * sy;
if mirror_h {
m00 = -m00;
m10 = -m10;
tx = canvas_w - tx;
}
ViewTransform {
matrix: [
[m00, m01, canvas_w, 0.0],
[m10, m11, canvas_h, 0.0],
[tx, ty, 1.0, 0.0],
],
bg: DEFAULT_WORKSPACE_BG,
flags: [0.0; 4],
}
}
pub fn screen_to_window_local(&self, screen_x: f32, screen_y: f32) -> (f32, f32) {
apply_2x3(&self.matrix, screen_x, screen_y)
}
pub fn screen_to_plane_matrix(&self, origin_x: f32, origin_y: f32) -> [[f32; 4]; 3] {
let mut m = self.matrix;
m[2][0] += origin_x;
m[2][1] += origin_y;
m
}
pub fn plane_to_screen_matrix(&self, origin_x: f32, origin_y: f32) -> [[f32; 4]; 3] {
invert_2x3(&self.screen_to_plane_matrix(origin_x, origin_y))
}
pub fn screen_to_plane(
&self,
screen_x: f32,
screen_y: f32,
origin_x: f32,
origin_y: f32,
) -> (f32, f32) {
apply_2x3(
&self.screen_to_plane_matrix(origin_x, origin_y),
screen_x,
screen_y,
)
}
}
#[allow(clippy::too_many_arguments)]
pub fn compute_view_matrices(
pan_x: f32,
pan_y: f32,
zoom: f32,
rotation: f32,
mirror_h: bool,
screen_w: f32,
screen_h: f32,
canvas_origin_x: f32,
canvas_origin_y: f32,
canvas_w: f32,
canvas_h: f32,
) -> [f32; 12] {
let vt = ViewTransform::from_pan_zoom_rotate(
pan_x, pan_y, zoom, rotation, mirror_h, screen_w, screen_h, canvas_w, canvas_h,
);
let s2p = vt.screen_to_plane_matrix(canvas_origin_x, canvas_origin_y);
let p2s = vt.plane_to_screen_matrix(canvas_origin_x, canvas_origin_y);
[
s2p[0][0], s2p[1][0], s2p[2][0], s2p[0][1], s2p[1][1], s2p[2][1], p2s[0][0], p2s[0][1], p2s[2][0], p2s[1][0], p2s[1][1], p2s[2][1],
]
}
fn apply_2x3(m: &[[f32; 4]; 3], x: f32, y: f32) -> (f32, f32) {
(
m[0][0] * x + m[1][0] * y + m[2][0],
m[0][1] * x + m[1][1] * y + m[2][1],
)
}
pub(crate) fn invert_2x3(m: &[[f32; 4]; 3]) -> [[f32; 4]; 3] {
let m00 = m[0][0];
let m01 = m[0][1];
let m10 = m[1][0];
let m11 = m[1][1];
let tx = m[2][0];
let ty = m[2][1];
let det = m00 * m11 - m10 * m01;
if det.abs() < 1e-12 {
return [
[1.0, 0.0, 0.0, 0.0],
[0.0, 1.0, 0.0, 0.0],
[0.0, 0.0, 0.0, 0.0],
];
}
let inv_det = 1.0 / det;
let f00 = m11 * inv_det;
let f01 = -m10 * inv_det;
let f10 = -m01 * inv_det;
let f11 = m00 * inv_det;
let ftx = -(f00 * tx + f01 * ty);
let fty = -(f10 * tx + f11 * ty);
[
[f00, f01, 0.0, 0.0],
[f10, f11, 0.0, 0.0],
[ftx, fty, 0.0, 0.0],
]
}
#[cfg(test)]
mod tests {
use super::*;
fn approx(a: f32, b: f32) -> bool {
(a - b).abs() < 1e-3
}
fn fwd_apply(m: &[[f32; 4]; 3], x: f32, y: f32) -> (f32, f32) {
(
m[0][0] * x + m[0][1] * y + m[2][0],
m[1][0] * x + m[1][1] * y + m[2][1],
)
}
#[test]
fn mirror_h_keeps_center_fixed() {
let t = ViewTransform::from_pan_zoom_rotate(
0.0, 0.0, 1.0, 0.0, true, 800.0, 600.0, 400.0, 300.0,
);
let (cx, cy) = t.screen_to_window_local(400.0, 300.0);
assert!(approx(cx, 200.0), "mirrored center cx was {cx}");
assert!(approx(cy, 150.0), "mirrored center cy was {cy}");
}
#[test]
fn mirror_h_reflects_canvas_x_around_canvas_center() {
let pan_x = 37.0;
let pan_y = -12.0;
let zoom = 1.7;
let rot = 0.4;
let sw = 800.0;
let sh = 600.0;
let cw = 400.0;
let ch = 300.0;
let unmirrored =
ViewTransform::from_pan_zoom_rotate(pan_x, pan_y, zoom, rot, false, sw, sh, cw, ch);
let mirrored =
ViewTransform::from_pan_zoom_rotate(pan_x, pan_y, zoom, rot, true, sw, sh, cw, ch);
for &(x, y) in &[(123.0, 88.0), (600.0, 450.0), (0.0, 0.0), (sw, sh)] {
let (ux, uy) = unmirrored.screen_to_window_local(x, y);
let (mx, my) = mirrored.screen_to_window_local(x, y);
assert!(
approx(ux + mx, cw),
"x={x} y={y}: ux+mx={} (want {cw})",
ux + mx
);
assert!(approx(uy, my), "x={x} y={y}: uy={uy} my={my}");
}
}
#[test]
fn plane_forward_matches_window_local_paint_path() {
let (ox, oy) = (8.0_f32, 4.0_f32);
let vt = ViewTransform::from_pan_zoom_rotate(
17.0, -9.0, 1.6, 0.3, false, 800.0, 600.0, 40.0, 40.0,
);
let plane_fwd = vt.plane_to_screen_matrix(ox, oy);
let window_fwd = vt.plane_to_screen_matrix(0.0, 0.0);
for &(px, py) in &[(28.0, 24.0), (8.0, 4.0), (47.0, 43.0)] {
let via_plane = fwd_apply(&plane_fwd, px, py);
let via_paint = fwd_apply(&window_fwd, px - ox, py - oy);
assert!(
approx(via_plane.0, via_paint.0) && approx(via_plane.1, via_paint.1),
"plane forward {via_plane:?} != paint path {via_paint:?} at ({px},{py})"
);
}
}
#[test]
fn screen_plane_round_trip_and_origin_shift() {
let vt = ViewTransform::from_pan_zoom_rotate(
5.0, 3.0, 1.3, -0.2, true, 800.0, 600.0, 50.0, 70.0,
);
let fwd = vt.plane_to_screen_matrix(12.0, -6.0);
for &(sx, sy) in &[(123.0, 88.0), (400.0, 300.0), (0.0, 0.0)] {
let p = vt.screen_to_plane(sx, sy, 12.0, -6.0);
let back = fwd_apply(&fwd, p.0, p.1);
assert!(
approx(back.0, sx) && approx(back.1, sy),
"round-trip ({sx},{sy}) -> {p:?} -> {back:?}"
);
}
let a = vt.screen_to_plane(200.0, 150.0, 0.0, 0.0);
let b = vt.screen_to_plane(200.0, 150.0, 40.0, 15.0);
assert!(approx(b.0 - a.0, 40.0) && approx(b.1 - a.1, 15.0));
}
#[test]
fn compute_view_matrices_matches_view_transform() {
let (pan_x, pan_y, zoom, rot, mir) = (11.0, -4.0, 1.4, 0.25, false);
let (sw, sh, cw, ch) = (800.0, 600.0, 50.0, 70.0);
let (ox, oy) = (9.0, -3.0);
let m = compute_view_matrices(pan_x, pan_y, zoom, rot, mir, sw, sh, ox, oy, cw, ch);
let vt = ViewTransform::from_pan_zoom_rotate(pan_x, pan_y, zoom, rot, mir, sw, sh, cw, ch);
let row = |m: &[f32; 12], o: usize, x: f32, y: f32| {
(
m[o] * x + m[o + 1] * y + m[o + 2],
m[o + 3] * x + m[o + 4] * y + m[o + 5],
)
};
for &(sx, sy) in &[(123.0, 88.0), (400.0, 300.0)] {
let want = vt.screen_to_plane(sx, sy, ox, oy);
let got = row(&m, 0, sx, sy);
assert!(
approx(got.0, want.0) && approx(got.1, want.1),
"screen→plane pack {got:?} != {want:?}"
);
let back = row(&m, 6, want.0, want.1);
assert!(
approx(back.0, sx) && approx(back.1, sy),
"plane→screen pack did not round-trip: {back:?} != ({sx},{sy})"
);
}
}
}