use crate::{Actor, ActorBehavior, Message, Port};
use anyhow::{Error, Result};
use reflow_actor::{message::EncodableValue, ActorContext};
use reflow_actor_macro::actor;
use serde_json::json;
use std::collections::HashMap;
#[actor(ImageToHeightmapActor, inports::<10>(input), outports::<1>(output, metadata, error), state(MemoryState))]
pub async fn image_to_heightmap_actor(
ctx: ActorContext,
) -> Result<HashMap<String, Message>, Error> {
let payload = ctx.get_payload();
let config = ctx.get_config_hashmap();
let bytes = match payload.get("input") {
Some(Message::Bytes(b)) => b.clone(),
_ => return Ok(error_output("Expected Bytes on input port")),
};
let img = match image::load_from_memory(&bytes) {
Ok(img) => img,
Err(_) => {
let channels = config.get("channels").and_then(|v| v.as_u64()).unwrap_or(4) as usize;
let total_pixels = bytes.len() / channels;
let width = config
.get("width")
.and_then(|v| v.as_u64())
.unwrap_or((total_pixels as f64).sqrt() as u64) as usize;
let height = total_pixels / width;
let mut grid = Vec::with_capacity(width * height);
for i in 0..width * height {
let offset = i * channels;
if offset + 2 < bytes.len() {
let r = bytes[offset] as f64 / 255.0;
let g = bytes[offset + 1] as f64 / 255.0;
let b = bytes[offset + 2] as f64 / 255.0;
grid.push(0.299 * r + 0.587 * g + 0.114 * b);
} else {
grid.push(0.0);
}
}
let grid_bytes: Vec<u8> = grid.iter().flat_map(|v| v.to_le_bytes()).collect();
let mut out = HashMap::new();
out.insert("output".to_string(), Message::bytes(grid_bytes));
out.insert(
"metadata".to_string(),
Message::object(EncodableValue::from(json!({
"width": width, "height": height, "dataType": "f64",
}))),
);
return Ok(out);
}
};
let (width, height) = (img.width() as usize, img.height() as usize);
let rgba = img.to_rgba8();
let pixels = rgba.as_raw();
let mut grid = Vec::with_capacity(width * height);
for i in 0..width * height {
let r = pixels[i * 4] as f64 / 255.0;
let g = pixels[i * 4 + 1] as f64 / 255.0;
let b = pixels[i * 4 + 2] as f64 / 255.0;
grid.push(0.299 * r + 0.587 * g + 0.114 * b);
}
let grid_bytes: Vec<u8> = grid.iter().flat_map(|v| v.to_le_bytes()).collect();
let mut out = HashMap::new();
out.insert("output".to_string(), Message::bytes(grid_bytes));
out.insert(
"metadata".to_string(),
Message::object(EncodableValue::from(json!({
"width": width, "height": height, "dataType": "f64",
"min": grid.iter().cloned().fold(f64::MAX, f64::min),
"max": grid.iter().cloned().fold(f64::MIN, f64::max),
}))),
);
Ok(out)
}
#[actor(HeightmapToImageActor, inports::<10>(input), outports::<1>(output, metadata, error), state(MemoryState))]
pub async fn heightmap_to_image_actor(
ctx: ActorContext,
) -> Result<HashMap<String, Message>, Error> {
let payload = ctx.get_payload();
let config = ctx.get_config_hashmap();
let bytes = match payload.get("input") {
Some(Message::Bytes(b)) => b.clone(),
_ => return Ok(error_output("Expected Bytes on input port")),
};
let grid: Vec<f64> = bytes
.chunks_exact(8)
.map(|b| f64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]))
.collect();
let total = grid.len();
let width = config
.get("width")
.and_then(|v| v.as_u64())
.unwrap_or((total as f64).sqrt() as u64) as usize;
let height = total / width;
let min = grid.iter().cloned().fold(f64::MAX, f64::min);
let max = grid.iter().cloned().fold(f64::MIN, f64::max);
let range = if (max - min).abs() > 1e-10 {
max - min
} else {
1.0
};
let color_mode = config
.get("colorMode")
.and_then(|v| v.as_str())
.unwrap_or("grayscale");
let mut rgba = Vec::with_capacity(width * height * 4);
for &v in &grid {
let norm = ((v - min) / range).clamp(0.0, 1.0);
match color_mode {
"terrain" => {
let (r, g, b) = if norm < 0.3 {
(30, 80, (180.0 + norm * 200.0) as u8)
} else if norm < 0.6 {
let t = (norm - 0.3) / 0.3;
(
(30.0 + t * 100.0) as u8,
(120.0 + t * 60.0) as u8,
(40.0 + t * 30.0) as u8,
)
} else if norm < 0.85 {
let t = (norm - 0.6) / 0.25;
(
(130.0 + t * 60.0) as u8,
(100.0 - t * 40.0) as u8,
(50.0 + t * 20.0) as u8,
)
} else {
let t = (norm - 0.85) / 0.15;
let g = (200.0 + t * 55.0) as u8;
(g, g, g)
};
rgba.extend_from_slice(&[r, g, b, 255]);
}
_ => {
let v = (norm * 255.0) as u8;
rgba.extend_from_slice(&[v, v, v, 255]);
}
}
}
let mut out = HashMap::new();
out.insert("output".to_string(), Message::bytes(rgba));
out.insert(
"metadata".to_string(),
Message::object(EncodableValue::from(json!({
"width": width, "height": height,
"format": "RGBA8", "channels": 4,
"colorMode": color_mode,
}))),
);
Ok(out)
}
fn error_output(msg: &str) -> HashMap<String, Message> {
let mut out = HashMap::new();
out.insert("error".to_string(), Message::Error(msg.to_string().into()));
out
}