docs/bevy/examples/asset/asset_saving.rs 8.7 K · 299 lines · raw · history

1 //! This example demonstrates how to save assets in the common case where the asset contains no
2 //! subassets.
3
4 use 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
20 fn 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
36 const ASSET_PATH: &str = "art_project.png";
37
38 fn 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.
68 fn 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)]
80 struct ImageToSave(Handle<Image>);
81
82 #[derive(Component)]
83 struct SpriteToSave;
84
85 fn 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
102 LMB - Draw with selected color
103 F5 - 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)]
189 struct TryPlot {
190 entity: Entity,
191 location: Location,
192 }
193
194 fn on_drag_start(drag_start: On<PointerDragStart>, mut commands: Commands) {
195 commands.trigger(TryPlot {
196 entity: drag_start.entity,
197 location: drag_start.pointer.location(),
198 });
199 }
200
201 fn on_drag(drag: On<PointerDrag>, mut commands: Commands) {
202 commands.trigger(TryPlot {
203 entity: drag.entity,
204 location: drag.pointer.location(),
205 });
206 }
207
208 fn try_plot(
209 try_plot: 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(try_plot.entity) else {
217 return;
218 };
219 let (camera, camera_transform) = camera.into_inner();
220 let Ok(world_position) =
221 camera.viewport_to_world_2d(camera_transform, try_plot.location.position)
222 else {
223 return;
224 };
225 let relative_to_sprite = sprite_transform
226 .affine()
227 .inverse()
228 .transform_point3(world_position.extend(0.0));
229 let Ok(pixel_space) = sprite.compute_pixel_space_point(
230 relative_to_sprite.xy(),
231 *anchor,
232 &images,
233 &texture_atlases,
234 ) else {
235 return;
236 };
237 let pixel_coordinates = pixel_space.floor().as_uvec2();
238 let mut image = images.get_mut(&sprite.image).unwrap();
239 // For an actual drawing app, you'd at least draw a line from the last point, but this is
240 // simpler.
241 image
242 .set_color_at(pixel_coordinates.x, pixel_coordinates.y, draw_color.0)
243 .unwrap();
244 }
245
246 #[derive(Resource, Default)]
247 struct DrawColor(Color);
248
249 #[derive(Component)]
250 struct SelectableColor;
251
252 #[derive(Component)]
253 struct Selected;
254
255 const NORMAL_COLOR: Color = Color::BLACK;
256 const HIGHLIGHT_COLOR: Color = Color::Srgba(tailwind::NEUTRAL_500);
257 const SELECTED_COLOR: Color = Color::Srgba(tailwind::RED_600);
258
259 fn on_enter_selectable(
260 event: On<PointerEnter>,
261 mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
262 ) {
263 let Ok(mut border) = border.get_mut(event.entity) else {
264 return;
265 };
266
267 *border = BorderColor::all(HIGHLIGHT_COLOR);
268 }
269
270 fn on_leave_selectable(
271 event: On<PointerLeave>,
272 mut border: Query<&mut BorderColor, (With<SelectableColor>, Without<Selected>)>,
273 ) {
274 let Ok(mut border) = border.get_mut(event.entity) else {
275 return;
276 };
277
278 *border = BorderColor::all(NORMAL_COLOR);
279 }
280
281 fn on_press_selectable(
282 event: On<PointerPress>,
283 mut borders: Query<(Entity, &mut BorderColor, &BackgroundColor), With<SelectableColor>>,
284 mut draw_color: ResMut<DrawColor>,
285 mut commands: Commands,
286 ) {
287 if !borders.contains(event.entity) {
288 return;
289 }
290 for (entity, mut border, _) in borders.iter_mut() {
291 commands.entity(entity).remove::<Selected>();
292 *border = BorderColor::all(NORMAL_COLOR);
293 }
294 let (_, mut border, background_color) = borders.get_mut(event.entity).unwrap();
295 *border = BorderColor::all(SELECTED_COLOR);
296 commands.entity(event.entity).insert(Selected);
297
298 draw_color.0 = background_color.0;
299 }