//! This example demonstrates how to save assets in the common case where the asset contains no //! subassets. use bevy::{ asset::{ saver::{save_using_saver, SavedAsset}, RenderAssetUsages, }, camera::ScalingMode, color::palettes::tailwind, image::{ImageLoaderSettings, ImageSampler, ImageSaver, ImageSaverSettings}, input::common_conditions::input_just_pressed, picking::pointer::Location, prelude::*, render::render_resource::{Extent3d, TextureDimension, TextureFormat}, sprite::Anchor, tasks::IoTaskPool, }; fn main() { App::new() .add_plugins(DefaultPlugins.set(AssetPlugin { // This is just overriding the default asset paths to scope this to the correct example // folder. You can generally skip this in your own projects. file_path: "examples/asset/saved_assets".to_string(), ..Default::default() })) .add_plugins(image_drawing_plugin) .add_systems( PreUpdate, perform_save.run_if(input_just_pressed(KeyCode::F5)), ) .run(); } const ASSET_PATH: &str = "art_project.png"; fn perform_save( image_to_save: Res, images: Res>, asset_server: Res, ) { let image = images.get(&image_to_save.0).unwrap(); let image = image.clone(); let asset_server = asset_server.clone(); IoTaskPool::get() .spawn(async move { match save_using_saver( asset_server.clone(), &ImageSaver, &ASSET_PATH.into(), SavedAsset::from_asset(&image), &ImageSaverSettings::default(), ) .await { Ok(()) => info!("Completed save of {ASSET_PATH}"), Err(err) => error!("Failed to save asset: {err}"), } }) .detach(); } /// Plugin for doing image drawing. /// /// This doesn't really have anything to do with asset saving, but provides a real-use case. fn image_drawing_plugin(app: &mut App) { app.add_systems(Startup, setup) .add_observer(on_drag_start) .add_observer(on_drag) .add_observer(try_plot) .init_resource::() .add_observer(on_enter_selectable) .add_observer(on_leave_selectable) .add_observer(on_press_selectable); } #[derive(Resource)] struct ImageToSave(Handle); #[derive(Component)] struct SpriteToSave; fn setup( mut commands: Commands, asset_server: Res, mut images: ResMut>, ) { commands.spawn(( Camera2d, Projection::Orthographic(OrthographicProjection { scaling_mode: ScalingMode::FixedVertical { viewport_height: 125.0, }, ..OrthographicProjection::default_2d() }), )); commands.spawn(Text( r"Select a color from the palette at the bottom LMB - Draw with selected color F5 - Save image" .into(), )); let handle = asset_server .load_builder() .with_settings(|settings: &mut ImageLoaderSettings| { settings.sampler = ImageSampler::nearest(); }) .load(ASSET_PATH); commands.spawn(( Sprite { image: handle.clone(), ..Default::default() }, SpriteToSave, Pickable::default(), )); // We're doing something a little cursed here: we initiate a load, and then insert a default // image into that handle. If the load succeeds, the image will be replaced with the loaded // contents. If it fails, the default image will remain. In real code, you likely want to poll // `AssetServer::load_state` and only insert this on load failure. images .insert(&handle, { let mut image = Image::new_fill( Extent3d { width: 100, height: 100, depth_or_array_layers: 1, }, TextureDimension::D2, &[0, 0, 0, 255], TextureFormat::Rgba8Unorm, RenderAssetUsages::all(), ); image.sampler = ImageSampler::nearest(); image }) .unwrap(); commands.insert_resource(ImageToSave(handle)); let container = commands .spawn(( Node { width: percent(100), height: percent(100), align_items: AlignItems::End, justify_content: JustifyContent::Center, ..Default::default() }, Pickable::IGNORE, )) .id(); for color in [ Color::WHITE, Color::Srgba(tailwind::RED_500), Color::Srgba(tailwind::ORANGE_500), Color::Srgba(tailwind::YELLOW_500), Color::Srgba(tailwind::GREEN_500), Color::Srgba(tailwind::BLUE_500), Color::Srgba(tailwind::INDIGO_500), Color::Srgba(tailwind::VIOLET_500), Color::BLACK, ] { let mut entity = commands.spawn(( Node { width: vw(5), height: vh(5), border: px(5).all(), ..Default::default() }, SelectableColor, BackgroundColor(color), BorderColor::all(NORMAL_COLOR), ChildOf(container), )); if color == Color::WHITE { entity.insert((Selected, BorderColor::all(SELECTED_COLOR))); } } } #[derive(EntityEvent)] struct TryPlot { entity: Entity, location: Location, } fn on_drag_start(drag_start: On, mut commands: Commands) { commands.trigger(TryPlot { entity: drag_start.entity, location: drag_start.pointer.location(), }); } fn on_drag(drag: On, mut commands: Commands) { commands.trigger(TryPlot { entity: drag.entity, location: drag.pointer.location(), }); } fn try_plot( try_plot: On, sprite: Query<(&Sprite, &Anchor, &GlobalTransform), With>, camera: Single<(&Camera, &GlobalTransform)>, texture_atlases: Res>, draw_color: Res, mut images: ResMut>, ) { let Ok((sprite, anchor, sprite_transform)) = sprite.get(try_plot.entity) else { return; }; let (camera, camera_transform) = camera.into_inner(); let Ok(world_position) = camera.viewport_to_world_2d(camera_transform, try_plot.location.position) else { return; }; let relative_to_sprite = sprite_transform .affine() .inverse() .transform_point3(world_position.extend(0.0)); let Ok(pixel_space) = sprite.compute_pixel_space_point( relative_to_sprite.xy(), *anchor, &images, &texture_atlases, ) else { return; }; let pixel_coordinates = pixel_space.floor().as_uvec2(); let mut image = images.get_mut(&sprite.image).unwrap(); // For an actual drawing app, you'd at least draw a line from the last point, but this is // simpler. image .set_color_at(pixel_coordinates.x, pixel_coordinates.y, draw_color.0) .unwrap(); } #[derive(Resource, Default)] struct DrawColor(Color); #[derive(Component)] struct SelectableColor; #[derive(Component)] struct Selected; const NORMAL_COLOR: Color = Color::BLACK; const HIGHLIGHT_COLOR: Color = Color::Srgba(tailwind::NEUTRAL_500); const SELECTED_COLOR: Color = Color::Srgba(tailwind::RED_600); fn on_enter_selectable( event: On, mut border: Query<&mut BorderColor, (With, Without)>, ) { let Ok(mut border) = border.get_mut(event.entity) else { return; }; *border = BorderColor::all(HIGHLIGHT_COLOR); } fn on_leave_selectable( event: On, mut border: Query<&mut BorderColor, (With, Without)>, ) { let Ok(mut border) = border.get_mut(event.entity) else { return; }; *border = BorderColor::all(NORMAL_COLOR); } fn on_press_selectable( event: On, mut borders: Query<(Entity, &mut BorderColor, &BackgroundColor), With>, mut draw_color: ResMut, mut commands: Commands, ) { if !borders.contains(event.entity) { return; } for (entity, mut border, _) in borders.iter_mut() { commands.entity(entity).remove::(); *border = BorderColor::all(NORMAL_COLOR); } let (_, mut border, background_color) = borders.get_mut(event.entity).unwrap(); *border = BorderColor::all(SELECTED_COLOR); commands.entity(event.entity).insert(Selected); draw_color.0 = background_color.0; }