pittore 0.2.4

Simple toolkit for 2D visualization based on wgpu.
Documentation
use glam::{Mat4, Vec2, Vec3};

use super::bind_group::AsBindGroup;

#[repr(C)]
#[derive(Debug, Default, Copy, Clone, bytemuck::Zeroable, bytemuck::Pod)]
pub struct View {
    pub clip_from_world: Mat4,
    _padding: [Mat4; 3],
}

impl AsBindGroup for View {
    fn bind_group_layout(device: &wgpu::Device) -> wgpu::BindGroupLayout
    where
        Self: Sized,
    {
        device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
            label: Some("view_bind_group_layout"),
            entries: &[wgpu::BindGroupLayoutEntry {
                binding: 0,
                visibility: wgpu::ShaderStages::VERTEX,
                ty: wgpu::BindingType::Buffer {
                    ty: wgpu::BufferBindingType::Uniform,
                    has_dynamic_offset: true,
                    min_binding_size: None,
                },
                count: None,
            }],
        })
    }

    fn as_bind_group(&self, render_state: &crate::RenderState) -> wgpu::BindGroup {
        render_state
            .device
            .create_bind_group(&wgpu::BindGroupDescriptor {
                label: Some("view_bind_group"),
                layout: &Self::bind_group_layout(&render_state.device),
                entries: &[wgpu::BindGroupEntry {
                    binding: 0,
                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
                        buffer: render_state.buffers.get::<View>().unwrap(),
                        offset: 0,
                        size: Some((size_of::<View>() as u64).try_into().unwrap()),
                    }),
                }],
            })
    }
}

#[derive(Debug, Clone)]
pub struct Camera {
    pub position: Vec3,
    pub projection: Projection,
    pub scale: f32,
    pub scaling_mode: ScalingMode,
}

impl Default for Camera {
    fn default() -> Self {
        Camera::default_2d()
    }
}

impl Camera {
    pub fn default_2d() -> Self {
        Camera {
            position: Vec3::ZERO,
            projection: Projection::Orthographic,
            scale: 1.0,
            scaling_mode: ScalingMode::WindowSize,
        }
    }

    pub fn view(&self, surface_size: Vec2) -> View {
        let render_size = self.scaling_mode.render_size(surface_size);
        let scale = 2.0 / render_size * self.scale;
        let translation = -self.position;

        let clip_from_world =
            Mat4::from_scale((scale, 1.0).into()) * Mat4::from_translation(translation);

        View {
            clip_from_world,
            ..Default::default()
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub enum Projection {
    Orthographic,
}

#[derive(Debug, Clone, Copy)]
pub enum ScalingMode {
    /// Adjust the viewport to the window size.
    WindowSize,
    /// Fix the viewport size.
    Fixed(Vec2),
    /// Fix the viewport width. Height will be adjusted to match aspect ratio.
    FixedHorizontal(f32),
    /// Fix the viewport height. Width will be adjusted to match aspect ratio.
    FixedVertical(f32),
    /// Keep the aspect ratio while the axes cannot be smaller than given minimum.
    /// Useful for fitting contents which has fixed aspect ratio in a resizable window.
    AutoMin(Vec2),
    /// Keep the aspect ratio while the axes cannot be greater than given maximum.
    AutoMax(Vec2),
}

impl ScalingMode {
    pub fn render_size(self, surface_size: Vec2) -> Vec2 {
        match self {
            ScalingMode::WindowSize => surface_size,
            ScalingMode::Fixed(size) => size,
            ScalingMode::FixedHorizontal(width) => surface_size * width / surface_size.x,
            ScalingMode::FixedVertical(height) => surface_size * height / surface_size.y,
            ScalingMode::AutoMin(size) => surface_size * (size / surface_size).max_element(),
            ScalingMode::AutoMax(size) => surface_size * (size / surface_size).min_element(),
        }
    }
}