1use std::any::Any;
4use std::sync::Arc;
5
6use viewport_lib::ItemSettings;
7use viewport_lib::plugin_api::PluginItemCollection;
8use viewport_lib::renderer::PickId;
9
10pub const LAYER_COUNT: usize = 8;
13
14#[derive(Copy, Clone, Debug)]
23pub struct TerrainLayer {
24 pub albedo: [f32; 3],
25 pub metallic: f32,
26 pub roughness: f32,
27 pub height_bias: f32,
28 pub textured: bool,
30 pub normal_mapped: bool,
33 pub normal_scale: f32,
36 pub mask_mapped: bool,
40 pub mask_remap_min: [f32; 4],
43 pub mask_remap_max: [f32; 4],
45 pub uv_scale: [f32; 2],
49 pub uv_offset: [f32; 2],
51}
52
53impl Default for TerrainLayer {
54 fn default() -> Self {
55 Self {
56 albedo: [0.5, 0.5, 0.5],
57 metallic: 0.0,
58 roughness: 0.9,
59 height_bias: 0.0,
60 textured: false,
61 normal_mapped: false,
62 normal_scale: 1.0,
63 mask_mapped: false,
64 mask_remap_min: [0.0, 0.0, 0.0, 0.0],
65 mask_remap_max: [1.0, 1.0, 1.0, 1.0],
66 uv_scale: [1.0, 1.0],
67 uv_offset: [0.0, 0.0],
68 }
69 }
70}
71
72#[derive(Clone)]
86pub struct LayerTextures {
87 rgba: Arc<[u8]>,
88 pub dims: [u32; 2],
90 pub version: u64,
92}
93
94impl LayerTextures {
95 pub fn new(sources: &[Vec<u8>], dims: [u32; 2]) -> Option<Self> {
102 if sources.is_empty() || sources.len() > LAYER_COUNT {
103 return None;
104 }
105 let slot_len = (dims[0] as usize) * (dims[1] as usize) * 4;
106 if slot_len == 0 {
107 return None;
108 }
109 for buf in sources {
110 if buf.len() != slot_len {
111 return None;
112 }
113 }
114 let mut rgba = vec![0u8; slot_len * LAYER_COUNT];
115 for (slot, src) in sources.iter().enumerate() {
116 let start = slot * slot_len;
117 rgba[start..start + slot_len].copy_from_slice(src);
118 }
119 Some(Self {
120 rgba: Arc::from(rgba.into_boxed_slice()),
121 dims,
122 version: 0,
123 })
124 }
125
126 pub fn rgba(&self) -> &[u8] {
129 &self.rgba
130 }
131}
132
133#[derive(Clone)]
142pub struct SplatmapData {
143 rgba: Arc<[u8]>,
144 pub dims: [u32; 2],
146 pub version: u64,
151}
152
153impl SplatmapData {
154 pub fn new(rgba: Vec<u8>, dims: [u32; 2]) -> Self {
158 Self {
159 rgba: Arc::from(rgba.into_boxed_slice()),
160 dims,
161 version: 0,
162 }
163 }
164
165 pub fn rgba(&self) -> &[u8] {
167 &self.rgba
168 }
169
170 pub fn replace(&mut self, rgba: Vec<u8>, dims: [u32; 2]) {
173 self.rgba = Arc::from(rgba.into_boxed_slice());
174 self.dims = dims;
175 self.version = self.version.wrapping_add(1);
176 }
177
178 pub fn empty() -> Self {
181 Self::new(vec![0, 0, 0, 0], [1, 1])
182 }
183
184 pub fn solid_layer0() -> Self {
186 Self::new(vec![255, 0, 0, 0], [1, 1])
187 }
188
189 pub fn pack_layers(per_layer: &[Vec<u8>], dims: [u32; 2]) -> Option<[Self; 2]> {
201 let pixel_count = (dims[0] as usize) * (dims[1] as usize);
202 if per_layer.is_empty() {
203 return None;
204 }
205 for buf in per_layer {
206 if buf.len() != pixel_count {
207 return None;
208 }
209 }
210 let pack = |range: std::ops::Range<usize>| -> Vec<u8> {
211 let mut out = vec![0u8; pixel_count * 4];
212 for (slot, layer_idx) in range.enumerate() {
213 if let Some(src) = per_layer.get(layer_idx) {
214 for (px, value) in src.iter().enumerate() {
215 out[px * 4 + slot] = *value;
216 }
217 }
218 }
219 out
220 };
221 Some([Self::new(pack(0..4), dims), Self::new(pack(4..8), dims)])
222 }
223}
224
225#[derive(Clone)]
232pub struct TerrainItem {
233 pub heightmap: Arc<[u16]>,
235 pub heightmap_version: u64,
237 pub dims: [u32; 2],
239 pub world_size: [f32; 2],
241 pub height_range: [f32; 2],
244 pub origin: glam::Vec3,
246 pub surface_layers: [TerrainLayer; LAYER_COUNT],
248 pub layer_textures: Option<LayerTextures>,
252 pub normal_textures: Option<LayerTextures>,
257 pub mask_textures: Option<LayerTextures>,
262 pub splatmaps: [SplatmapData; 2],
264 pub height_blend_strength: f32,
266 pub height_blend_noise_scale: f32,
268 pub settings: ItemSettings,
269}
270
271impl TerrainItem {
272 pub fn new(heightmap: Vec<u16>, dims: [u32; 2]) -> Self {
275 Self {
276 heightmap: Arc::from(heightmap.into_boxed_slice()),
277 heightmap_version: 0,
278 dims,
279 world_size: [1.0, 1.0],
280 height_range: [0.0, 1.0],
281 origin: glam::Vec3::ZERO,
282 surface_layers: [TerrainLayer::default(); LAYER_COUNT],
283 layer_textures: None,
284 normal_textures: None,
285 mask_textures: None,
286 splatmaps: [SplatmapData::solid_layer0(), SplatmapData::empty()],
287 height_blend_strength: 0.0,
288 height_blend_noise_scale: 64.0,
289 settings: ItemSettings::default(),
290 }
291 }
292
293 pub fn replace_heightmap(&mut self, heightmap: Vec<u16>, dims: [u32; 2]) {
295 self.heightmap = Arc::from(heightmap.into_boxed_slice());
296 self.dims = dims;
297 self.heightmap_version = self.heightmap_version.wrapping_add(1);
298 }
299
300 pub fn from_u16_le_bytes(
310 bytes: &[u8],
311 dims: [u32; 2],
312 world_size: [f32; 2],
313 height_range: [f32; 2],
314 origin: glam::Vec3,
315 ) -> Option<Self> {
316 let expected = (dims[0] as usize) * (dims[1] as usize) * 2;
317 if bytes.len() != expected {
318 return None;
319 }
320 let mut heights: Vec<u16> = Vec::with_capacity(expected / 2);
321 for chunk in bytes.chunks_exact(2) {
322 heights.push(u16::from_le_bytes([chunk[0], chunk[1]]));
323 }
324 let mut item = Self::new(heights, dims);
325 item.world_size = world_size;
326 item.height_range = height_range;
327 item.origin = origin;
328 Some(item)
329 }
330
331 pub fn set_layers_from_descriptors(&mut self, descriptors: &[TerrainLayer]) {
338 let mut layers = [TerrainLayer::default(); LAYER_COUNT];
339 for (slot, src) in layers.iter_mut().zip(descriptors.iter().take(LAYER_COUNT)) {
340 *slot = *src;
341 }
342 self.surface_layers = layers;
343 }
344}
345
346pub struct TerrainCollection {
348 pub items: Vec<TerrainItem>,
349}
350
351impl PluginItemCollection for TerrainCollection {
352 fn len(&self) -> usize {
353 self.items.len()
354 }
355
356 fn item_settings(&self, index: usize) -> &ItemSettings {
357 &self.items[index].settings
358 }
359
360 fn pick_id(&self, index: usize) -> PickId {
361 self.items[index].settings.pick_id
362 }
363
364 fn as_any(&self) -> &dyn Any {
365 self
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn from_u16_le_bytes_round_trips_a_ramp() {
375 let dims = [4u32, 3u32];
376 let heights: Vec<u16> = (0u16..12u16).map(|i| i * 1000).collect();
377 let mut bytes = Vec::with_capacity(heights.len() * 2);
378 for h in &heights {
379 bytes.extend_from_slice(&h.to_le_bytes());
380 }
381 let item = TerrainItem::from_u16_le_bytes(
382 &bytes,
383 dims,
384 [10.0, 20.0],
385 [-5.0, 100.0],
386 glam::Vec3::new(1.0, 2.0, 3.0),
387 )
388 .expect("decode succeeds");
389 assert_eq!(item.dims, dims);
390 assert_eq!(item.world_size, [10.0, 20.0]);
391 assert_eq!(item.height_range, [-5.0, 100.0]);
392 assert_eq!(item.origin, glam::Vec3::new(1.0, 2.0, 3.0));
393 assert_eq!(item.heightmap.as_ref(), heights.as_slice());
394 }
395
396 #[test]
397 fn from_u16_le_bytes_rejects_wrong_length() {
398 let bytes = vec![0u8; 7];
399 assert!(
400 TerrainItem::from_u16_le_bytes(
401 &bytes,
402 [2, 2],
403 [1.0, 1.0],
404 [0.0, 1.0],
405 glam::Vec3::ZERO,
406 )
407 .is_none()
408 );
409 }
410
411 #[test]
412 fn pack_layers_routes_channels_correctly() {
413 let dims = [2u32, 2u32];
414 let pixel_count = 4;
415 let mut per_layer = Vec::new();
416 for layer in 0..6 {
417 per_layer.push(vec![(layer * 10) as u8; pixel_count]);
418 }
419 let [a, b] = SplatmapData::pack_layers(&per_layer, dims).expect("pack");
420 for px in 0..pixel_count {
421 assert_eq!(a.rgba()[px * 4], 0);
422 assert_eq!(a.rgba()[px * 4 + 1], 10);
423 assert_eq!(a.rgba()[px * 4 + 2], 20);
424 assert_eq!(a.rgba()[px * 4 + 3], 30);
425 assert_eq!(b.rgba()[px * 4], 40);
426 assert_eq!(b.rgba()[px * 4 + 1], 50);
427 assert_eq!(b.rgba()[px * 4 + 2], 0);
428 assert_eq!(b.rgba()[px * 4 + 3], 0);
429 }
430 }
431
432 #[test]
433 fn pack_layers_rejects_short_buffers() {
434 let dims = [2u32, 2u32];
435 let per_layer = vec![vec![0u8; 3]];
436 assert!(SplatmapData::pack_layers(&per_layer, dims).is_none());
437 }
438
439 #[test]
440 fn set_layers_from_descriptors_pads_with_default() {
441 let mut item = TerrainItem::new(vec![0; 1], [1, 1]);
442 let custom = TerrainLayer {
443 albedo: [0.1, 0.2, 0.3],
444 metallic: 0.5,
445 roughness: 0.4,
446 height_bias: 0.7,
447 textured: true,
448 normal_mapped: false,
449 normal_scale: 1.0,
450 mask_mapped: false,
451 mask_remap_min: [0.0, 0.0, 0.0, 0.0],
452 mask_remap_max: [1.0, 1.0, 1.0, 1.0],
453 uv_scale: [4.0, 4.0],
454 uv_offset: [0.0, 0.0],
455 };
456 item.set_layers_from_descriptors(&[custom]);
457 assert_eq!(item.surface_layers[0].albedo, [0.1, 0.2, 0.3]);
458 assert!(item.surface_layers[0].textured);
459 let default = TerrainLayer::default();
460 assert_eq!(item.surface_layers[1].albedo, default.albedo);
461 assert!(!item.surface_layers[1].textured);
462 assert_eq!(item.surface_layers[7].albedo, default.albedo);
463 }
464
465 #[test]
466 fn layer_textures_pack_slots_in_order() {
467 let dims = [2u32, 2u32];
468 let slot_len = 2 * 2 * 4;
469 let red = vec![255u8; slot_len];
470 let green: Vec<u8> = (0..slot_len)
471 .map(|i| if i % 4 == 1 { 255 } else { 0 })
472 .collect();
473 let tex = LayerTextures::new(&[red.clone(), green.clone()], dims).expect("pack");
474 assert_eq!(tex.dims, dims);
475 let packed = tex.rgba();
476 assert_eq!(packed.len(), slot_len * LAYER_COUNT);
477 assert_eq!(&packed[..slot_len], red.as_slice());
478 assert_eq!(&packed[slot_len..slot_len * 2], green.as_slice());
479 assert!(packed[slot_len * 2..].iter().all(|&b| b == 0));
481 }
482
483 #[test]
484 fn layer_textures_reject_bad_input() {
485 let dims = [2u32, 2u32];
486 assert!(LayerTextures::new(&[], dims).is_none());
487 assert!(LayerTextures::new(&[vec![0u8; 3]], dims).is_none());
488 let ok = vec![0u8; 16];
489 let too_many = vec![ok.clone(); LAYER_COUNT + 1];
490 assert!(LayerTextures::new(&too_many, dims).is_none());
491 }
492}