Skip to main content

asset_saving/
asset_saving.rs

1//! This example demonstrates how to save assets in the common case where the asset contains no
2//! subassets.
3
4use bevy::{
5    asset::{
6        saver::{save_using_saver, SavedAsset},
7        RenderAssetUsages,
8    },
9    camera::ScalingMode,
10    color::palettes::tailwind,
11    image::{ImageLoaderSettings, ImageSampler, ImageSaver, ImageSaverSettings},
12    input::common_conditions::input_just_pressed,
13    picking::pointer::Location,
14    prelude::*,
15    render::render_resource::{Extent3d, TextureDimension, TextureFormat},
16    sprite::Anchor,
17    tasks::IoTaskPool,
18};
19
20fn main() {
21    App::new()
22        .add_plugins(DefaultPlugins.set(AssetPlugin {
23            // This is just overriding the default asset paths to scope this to the correct example
24            // folder. You can generally skip this in your own projects.
25            file_path: "examples/asset/saved_assets".to_string(),
26            ..Default::default()
27        }))
28        .add_plugins(image_drawing_plugin)
29        .add_systems(
30            PreUpdate,
31            perform_save.run_if(input_just_pressed(KeyCode::F5)),
32        )
33        .run();
34}
35
36const ASSET_PATH: &str = "art_project.png";
37
38fn perform_save(
39    image_to_save: Res<ImageToSave>,
40    images: Res<Assets<Image>>,
41    asset_server: Res<AssetServer>,
42) {
43    let image = images.get(&image_to_save.0).unwrap();
44
45    let image = image.clone();
46    let asset_server = asset_server.clone();
47    IoTaskPool::get()
48        .spawn(async move {
49            match save_using_saver(
50                asset_server.clone(),
51                &ImageSaver,
52                &ASSET_PATH.into(),
53                SavedAsset::from_asset(&image),
54                &ImageSaverSettings::default(),
55            )
56            .await
57            {
58                Ok(()) => info!("Completed save of {ASSET_PATH}"),
59                Err(err) => error!("Failed to save asset: {err}"),
60            }
61        })
62        .detach();
63}
64
65/// Plugin for doing image drawing.
66///
67/// This doesn't really have anything to do with asset saving, but provides a real-use case.
68fn image_drawing_plugin(app: &mut App) {
69    app.add_systems(Startup, setup)
70        .add_observer(on_drag_start)
71        .add_observer(on_drag)
72        .add_observer(try_plot)
73        .init_resource::<DrawColor>()
74        .add_observer(on_enter_selectable)
75        .add_observer(on_leave_selectable)
76        .add_observer(on_press_selectable);
77}
78
79#[derive(Resource)]
80struct ImageToSave(Handle<Image>);
81
82#[derive(Component)]
83struct SpriteToSave;
84
85fn setup(
86    mut commands: Commands,
87    asset_server: Res<AssetServer>,
88    mut images: ResMut<Assets<Image>>,
89) {
90    commands.spawn((
91        Camera2d,
92        Projection::Orthographic(OrthographicProjection {
93            scaling_mode: ScalingMode::FixedVertical {
94                viewport_height: 125.0,
95            },
96            ..OrthographicProjection::default_2d()
97        }),
98    ));
99
100    commands.spawn(Text(
101        r"Select a color from the palette at the bottom
102LMB - Draw with selected color
103F5 - Save image"
104            .into(),
105    ));
106
107    let handle = asset_server
108        .load_builder()
109        .with_settings(|settings: &mut ImageLoaderSettings| {
110            settings.sampler = ImageSampler::nearest();
111        })
112        .load(ASSET_PATH);
113    commands.spawn((
114        Sprite {
115            image: handle.clone(),
116            ..Default::default()
117        },
118        SpriteToSave,
119        Pickable::default(),
120    ));
121
122    // We're doing something a little cursed here: we initiate a load, and then insert a default
123    // image into that handle. If the load succeeds, the image will be replaced with the loaded
124    // contents. If it fails, the default image will remain. In real code, you likely want to poll
125    // `AssetServer::load_state` and only insert this on load failure.
126    images
127        .insert(&handle, {
128            let mut image = Image::new_fill(
129                Extent3d {
130                    width: 100,
131                    height: 100,
132                    depth_or_array_layers: 1,
133                },
134                TextureDimension::D2,
135                &[0, 0, 0, 255],
136                TextureFormat::Rgba8Unorm,
137                RenderAssetUsages::all(),
138            );
139            image.sampler = ImageSampler::nearest();
140            image
141        })
142        .unwrap();
143
144    commands.insert_resource(ImageToSave(handle));
145
146    let container = commands
147        .spawn((
148            Node {
149                width: percent(100),
150                height: percent(100),
151                align_items: AlignItems::End,
152                justify_content: JustifyContent::Center,
153                ..Default::default()
154            },
155            Pickable::IGNORE,
156        ))
157        .id();
158
159    for color in [
160        Color::WHITE,
161        Color::Srgba(tailwind::RED_500),
162        Color::Srgba(tailwind::ORANGE_500),
163        Color::Srgba(tailwind::YELLOW_500),
164        Color::Srgba(tailwind::GREEN_500),
165        Color::Srgba(tailwind::BLUE_500),
166        Color::Srgba(tailwind::INDIGO_500),
167        Color::Srgba(tailwind::VIOLET_500),
168        Color::BLACK,
169    ] {
170        let mut entity = commands.spawn((
171            Node {
172                width: vw(5),
173                height: vh(5),
174                border: px(5).all(),
175                ..Default::default()
176            },
177            SelectableColor,
178            BackgroundColor(color),
179            BorderColor::all(NORMAL_COLOR),
180            ChildOf(container),
181        ));
182        if color == Color::WHITE {
183            entity.insert((Selected, BorderColor::all(SELECTED_COLOR)));
184        }
185    }
186}
187
188#[derive(EntityEvent)]
189struct TryPlot {
190    entity: Entity,
191    location: Location,
192}
193
194fn on_drag_start(event: On<Pointer<DragStart>>, mut commands: Commands) {
195    commands.trigger(TryPlot {
196        entity: event.entity,
197        location: event.pointer_location.clone(),
198    });
199}
200
201fn on_drag(event: On<Pointer<Drag>>, mut commands: Commands) {
202    commands.trigger(TryPlot {
203        entity: event.entity,
204        location: event.pointer_location.clone(),
205    });
206}
207
208fn try_plot(
209    event: On<TryPlot>,
210    sprite: Query<(&Sprite, &Anchor, &GlobalTransform), With<SpriteToSave>>,
211    camera: Single<(&Camera, &GlobalTransform)>,
212    texture_atlases: Res<Assets<TextureAtlasLayout>>,
213    draw_color: Res<DrawColor>,
214    mut images: ResMut<Assets<Image>>,
215) {
216    let Ok((sprite, anchor, sprite_transform)) = sprite.get(event.entity) else {
217        return;
218    };
219    let (camera, camera_transform) = camera.into_inner();
220    let Ok(world_position) = camera.viewport_to_world_2d(camera_transform, event.location.position)
221    else {
222        return;
223    };
224    let relative_to_sprite = sprite_transform
225        .affine()
226        .inverse()
227        .transform_point3(world_position.extend(0.0));
228    let Ok(pixel_space) = sprite.compute_pixel_space_point(
229        relative_to_sprite.xy(),
230        *anchor,
231        &images,
232        &texture_atlases,
233    ) else {
234        return;
235    };
236    let pixel_coordinates = pixel_space.floor().as_uvec2();
237    let mut image = images.get_mut(&sprite.image).unwrap();
238    // For an actual drawing app, you'd at least draw a line from the last point, but this is
239    // simpler.
240    image
241        .set_color_at(pixel_coordinates.x, pixel_coordinates.y, draw_color.0)
242        .unwrap();
243}
244
245#[derive(Resource, Default)]
246struct DrawColor(Color);
247
248#[derive(Component)]
249struct SelectableColor;
250
251#[derive(Component)]
252struct Selected;
253
254const NORMAL_COLOR: Color = Color::BLACK;
255const HIGHLIGHT_COLOR: Color = Color::Srgba(tailwind::NEUTRAL_500);
256const SELECTED_COLOR: Color = Color::Srgba(tailwind::RED_600);
257
258fn on_enter_selectable(
259    event: On<Pointer<Enter>>,
260    mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
261) {
262    let Ok(mut border) = border.get_mut(event.entity) else {
263        return;
264    };
265
266    *border = BorderColor::all(HIGHLIGHT_COLOR);
267}
268
269fn on_leave_selectable(
270    event: On<Pointer<Leave>>,
271    mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
272) {
273    let Ok(mut border) = border.get_mut(event.entity) else {
274        return;
275    };
276
277    *border = BorderColor::all(NORMAL_COLOR);
278}
279
280fn on_press_selectable(
281    event: On<Pointer<Press>>,
282    mut borders: Query<(Entity, &mut BorderColor, &BackgroundColor), With<SelectableColor>>,
283    mut draw_color: ResMut<DrawColor>,
284    mut commands: Commands,
285) {
286    if !borders.contains(event.entity) {
287        return;
288    }
289    for (entity, mut border, _) in borders.iter_mut() {
290        commands.entity(entity).remove::<Selected>();
291        *border = BorderColor::all(NORMAL_COLOR);
292    }
293    let (_, mut border, background_color) = borders.get_mut(event.entity).unwrap();
294    *border = BorderColor::all(SELECTED_COLOR);
295    commands.entity(event.entity).insert(Selected);
296
297    draw_color.0 = background_color.0;
298}