wde_terrain_editor/
lib.rs1use wde_logger::prelude::*;
3
4use bevy::prelude::*;
5use wde_terrain::prelude::*;
6
7use crate::{
8 paint::{brush::PaintBrush, paint_manager::PaintManagerPlugin},
9 processor::PaintProcessorPlugin,
10 ui_painter::TerrainEditorUIPlugin
11};
12
13mod paint;
14mod processor;
15mod ui_painter;
16
17pub struct TerrainEditorPlugin;
18impl Plugin for TerrainEditorPlugin {
19 fn build(&self, app: &mut App) {
20 app.add_plugins((
21 PaintManagerPlugin,
22 PaintProcessorPlugin,
23 TerrainEditorUIPlugin
24 ))
25 .init_resource::<SaveManager>()
26 .add_systems(PreStartup, init)
27 .add_systems(Update, handle_extracted_tiles)
28 .add_message::<ExtractedTileMessage>();
29 }
30}
31
32fn init(mut commands: Commands, asset_server: Res<AssetServer>) {
33 let entity = commands
34 .spawn((
35 Name::new("Terrain"),
36 Transform::default(),
37 Terrain::load("tests/terrain"),
38 TerrainRenderer::new(&asset_server),
39 TerrainPhysics::default()
40 ))
41 .id();
42
43 commands.spawn((
44 Name::new("Terrain Default Brush"),
45 PaintBrush::default(),
46 ChildOf(entity)
47 ));
48}
49
50#[derive(Resource, Default)]
51struct SaveManager {
52 saving: bool,
53 tiles_to_save: Vec<(ChunkPos, u32, u32)>
54}
55
56fn handle_extracted_tiles(
60 mut terrain: Query<&mut Terrain>,
61 mut save_manager: ResMut<SaveManager>,
62 mut extracted_tiles: MessageReader<ExtractedTileMessage>
63) {
64 let mut terrain = match terrain.single_mut() {
65 Ok(t) => t,
66 Err(_) => return
67 };
68
69 for message in extracted_tiles.read() {
70 let ExtractedTileMessage {
71 pos,
72 map_type,
73 splat_map_index,
74 data
75 } = message;
76
77 if *map_type == 0 {
79 terrain.dirty_physics.push((*pos, 0, 0, data.clone()));
80 }
81
82 if save_manager.saving {
84 let cur_dir = std::env::current_dir().unwrap();
85 let full_path = format!("{}/res/{}", cur_dir.display(), terrain.path);
86 match map_type {
87 0 => {
88 let path = format!("{}/heightmap_{}_{}.png", full_path, pos.x, pos.y);
89 if let Err(e) = save_png_from_channels(
90 &path,
91 data,
92 1,
93 (CHUNK_RENDER_SUBDIVISIONS, CHUNK_RENDER_SUBDIVISIONS)
94 ) {
95 error!("Failed to save heightmap ({}, {}): {}", pos.x, pos.y, e);
96 }
97 }
98 1 => {
99 let path = format!(
100 "{}/splatmap_{}_{}-{}.png",
101 full_path, pos.x, pos.y, splat_map_index
102 );
103 if let Err(e) = save_png_from_channels(
104 &path,
105 data,
106 4,
107 (CHUNK_RENDER_SUBDIVISIONS, CHUNK_RENDER_SUBDIVISIONS)
108 ) {
109 error!("Failed to save splatmap ({}, {}): {}", pos.x, pos.y, e);
110 }
111 }
112 _ => continue
113 }
114 save_manager
115 .tiles_to_save
116 .retain(|(p, t, s)| !(p == pos && t == map_type && s == splat_map_index));
117 }
118 }
119
120 if save_manager.saving && save_manager.tiles_to_save.is_empty() {
121 info!("Finished saving terrain.");
122 save_manager.saving = false;
123 }
124}
125
126fn save_png_from_channels(
127 path: &str,
128 data: &[u8],
129 channels: usize,
130 dimensions: (u32, u32)
131) -> Result<(), String> {
132 let rgba_data: Vec<u8> = match channels {
133 1 => data.iter().flat_map(|&v| [v, v, v, 255]).collect(),
134 4 => data.to_vec(),
135 _ => return Err("Unsupported number of channels".to_string())
136 };
137
138 let file =
139 std::fs::File::create(path).map_err(|e| format!("Failed to create file {path}: {e}"))?;
140 let w = std::io::BufWriter::new(file);
141 let mut encoder = png::Encoder::new(w, dimensions.0, dimensions.1);
142 encoder.set_color(png::ColorType::Rgba);
143 encoder.set_depth(png::BitDepth::Eight);
144 let mut writer = encoder
145 .write_header()
146 .map_err(|e| format!("Failed to write PNG header: {e}"))?;
147 writer
148 .write_image_data(&rgba_data)
149 .map_err(|e| format!("Failed to write PNG data: {e}"))
150}