jackdaw 0.3.1

A 3D level editor built with Bevy
Documentation
use bevy::{
    camera::{RenderTarget, visibility::RenderLayers},
    prelude::*,
    render::render_resource::TextureFormat,
};

use crate::colors;

#[derive(Component)]
pub struct PreviewSphere;

#[derive(Component)]
pub struct PreviewCamera;

#[derive(Resource)]
pub struct MaterialPreviewState {
    pub active_material: Option<Handle<StandardMaterial>>,
    pub orbit_yaw: f32,
    pub orbit_pitch: f32,
    pub zoom_distance: f32,
    pub preview_image: Handle<Image>,
}

impl Default for MaterialPreviewState {
    fn default() -> Self {
        Self {
            active_material: None,
            orbit_yaw: 0.5,
            orbit_pitch: -0.3,
            zoom_distance: 3.0,
            preview_image: Handle::default(),
        }
    }
}

const PREVIEW_LAYER: usize = 1;

pub fn setup_material_preview_scene(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut images: ResMut<Assets<Image>>,
    mut preview_state: ResMut<MaterialPreviewState>,
) {
    let preview_layer = RenderLayers::layer(PREVIEW_LAYER);

    let sphere = meshes.add(
        Sphere::new(1.0)
            .mesh()
            .ico(5)
            .unwrap()
            .with_generated_tangents()
            .unwrap(),
    );
    let mat = materials.add(StandardMaterial::default());

    commands.spawn((
        PreviewSphere,
        crate::EditorEntity,
        Mesh3d(sphere),
        MeshMaterial3d(mat),
        Transform::default(),
        Visibility::Inherited,
        preview_layer.clone(),
    ));

    commands.spawn((
        crate::EditorEntity,
        DirectionalLight {
            illuminance: 5000.0,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, -0.7, 0.5, 0.0)),
        Visibility::Inherited,
        preview_layer.clone(),
    ));

    // Fill light from opposite direction for balanced preview
    commands.spawn((
        crate::EditorEntity,
        DirectionalLight {
            illuminance: 2000.0,
            shadows_enabled: false,
            ..default()
        },
        Transform::from_rotation(Quat::from_euler(EulerRot::XYZ, 0.5, -2.5, 0.0)),
        Visibility::Inherited,
        preview_layer.clone(),
    ));

    let preview_image = Image::new_target_texture(
        256,
        256,
        TextureFormat::Rgba8Unorm,
        Some(TextureFormat::Rgba8UnormSrgb),
    );
    let preview_image_handle = images.add(preview_image);
    preview_state.preview_image = preview_image_handle.clone();

    commands.spawn((
        PreviewCamera,
        crate::EditorEntity,
        Camera3d::default(),
        Camera {
            order: -1,
            is_active: false,
            clear_color: ClearColorConfig::Custom(colors::MATERIAL_PREVIEW_BG),
            ..default()
        },
        RenderTarget::Image(preview_image_handle.into()),
        Transform::from_translation(Vec3::new(0.0, 0.0, 3.0)).looking_at(Vec3::ZERO, Vec3::Y),
        preview_layer,
    ));
}

pub fn update_preview_camera_transform(
    preview_state: Res<MaterialPreviewState>,
    mut camera_q: Query<&mut Transform, With<PreviewCamera>>,
) {
    if !preview_state.is_changed() || preview_state.active_material.is_none() {
        return;
    }

    let yaw = preview_state.orbit_yaw;
    let pitch = preview_state.orbit_pitch.clamp(-1.4, 1.4);
    let dist = preview_state.zoom_distance;

    let x = dist * pitch.cos() * yaw.sin();
    let y = dist * pitch.sin();
    let z = dist * pitch.cos() * yaw.cos();

    if let Ok(mut transform) = camera_q.single_mut() {
        *transform =
            Transform::from_translation(Vec3::new(x, y, z)).looking_at(Vec3::ZERO, Vec3::Y);
    }
}

pub fn update_active_preview_material(
    preview_state: Res<MaterialPreviewState>,
    mut sphere_q: Query<&mut MeshMaterial3d<StandardMaterial>, With<PreviewSphere>>,
    mut camera_q: Query<&mut Camera, With<PreviewCamera>>,
) {
    if !preview_state.is_changed() {
        return;
    }

    match &preview_state.active_material {
        Some(handle) => {
            if let Ok(mut sphere_mat) = sphere_q.single_mut() {
                sphere_mat.0 = handle.clone();
            }
            if let Ok(mut cam) = camera_q.single_mut() {
                cam.is_active = true;
            }
        }
        None => {
            if let Ok(mut cam) = camera_q.single_mut() {
                cam.is_active = false;
            }
        }
    }
}