use crate::{Actor, ActorBehavior, Message, Port};
use anyhow::{Error, Result};
use reflow_actor::{message::EncodableValue, ActorContext};
use reflow_actor_macro::actor;
use reflow_sdf::ir::SdfNode;
use std::collections::HashMap;
fn sdf_output(node: &SdfNode) -> HashMap<String, Message> {
let json = serde_json::to_value(node).unwrap_or_default();
let mut out = HashMap::new();
out.insert(
"sdf".to_string(),
Message::object(EncodableValue::from(json)),
);
out
}
#[actor(
SdfPathActor,
inports::<1>(),
outports::<1>(sdf, metadata, error),
state(MemoryState)
)]
pub async fn sdf_path_actor(ctx: ActorContext) -> Result<HashMap<String, Message>, Error> {
let config = ctx.get_config_hashmap();
let path_str = config
.get("path")
.and_then(|v| v.as_str())
.unwrap_or("M 0,0 L 5,0");
let profile: Vec<f32> = config
.get("profile")
.and_then(|v| v.as_array())
.map(|a| {
a.iter()
.filter_map(|v| v.as_f64().map(|f| f as f32))
.collect()
})
.unwrap_or_else(|| vec![0.1]);
let segments = config
.get("segments")
.and_then(|v| v.as_u64())
.unwrap_or(16) as usize;
let smoothness = config
.get("smoothness")
.and_then(|v| v.as_f64())
.unwrap_or(0.1) as f32;
let plane = config.get("plane").and_then(|v| v.as_str()).unwrap_or("xz");
let commands = parse_path(path_str);
if commands.is_empty() {
return Ok(error_out("Empty or invalid path"));
}
let points = sample_path(&commands, segments);
if points.len() < 2 {
return Ok(error_out("Path produced fewer than 2 points"));
}
let radii = interpolate_profile(&profile, points.len());
let mut pts3d: Vec<[f32; 3]> = Vec::with_capacity(points.len());
for (px, py) in &points {
let p = match plane {
"xy" => [*px, *py, 0.0],
"yz" => [0.0, *px, *py],
_ => [*px, 0.0, *py],
};
pts3d.push(p);
}
let node = SdfNode::tube_path(pts3d, radii, smoothness);
let mut out = sdf_output(&node);
out.insert(
"metadata".to_string(),
Message::object(EncodableValue::from(serde_json::json!({
"segments": points.len(),
"pathLength": path_length(&points),
}))),
);
Ok(out)
}
#[derive(Debug, Clone)]
pub enum PathCmd {
MoveTo(f32, f32),
LineTo(f32, f32),
CubicBezier(f32, f32, f32, f32, f32, f32), QuadBezier(f32, f32, f32, f32), }
pub fn parse_path(s: &str) -> Vec<PathCmd> {
let mut cmds = Vec::new();
let mut chars = s.chars().peekable();
let mut current_cmd = ' ';
while chars.peek().is_some() {
while chars.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
chars.next();
}
if let Some(&c) = chars.peek() {
if c.is_ascii_alphabetic() {
current_cmd = c;
chars.next();
}
}
while chars.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
chars.next();
}
match current_cmd.to_ascii_uppercase() {
'M' | 'L' => {
if let Some((x, y)) = parse_pair(&mut chars) {
let cmd = if current_cmd.eq_ignore_ascii_case(&'M') {
PathCmd::MoveTo(x, y)
} else {
PathCmd::LineTo(x, y)
};
cmds.push(cmd);
}
}
'C' => {
if let (Some((cx1, cy1)), Some((cx2, cy2)), Some((x, y))) = (
parse_pair(&mut chars),
parse_pair(&mut chars),
parse_pair(&mut chars),
) {
cmds.push(PathCmd::CubicBezier(cx1, cy1, cx2, cy2, x, y));
}
}
'Q' => {
if let (Some((cx, cy)), Some((x, y))) =
(parse_pair(&mut chars), parse_pair(&mut chars))
{
cmds.push(PathCmd::QuadBezier(cx, cy, x, y));
}
}
_ => {
chars.next(); }
}
}
cmds
}
fn parse_pair(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<(f32, f32)> {
let x = parse_number(chars)?;
while chars
.peek()
.map(|c| *c == ',' || c.is_whitespace())
.unwrap_or(false)
{
chars.next();
}
let y = parse_number(chars)?;
while chars
.peek()
.map(|c| *c == ',' || c.is_whitespace())
.unwrap_or(false)
{
chars.next();
}
Some((x, y))
}
fn parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<f32> {
let mut s = String::new();
if chars.peek() == Some(&'-') {
s.push('-');
chars.next();
}
while chars
.peek()
.map(|c| c.is_ascii_digit() || *c == '.')
.unwrap_or(false)
{
s.push(chars.next().unwrap());
}
s.parse().ok()
}
pub fn sample_path(cmds: &[PathCmd], n: usize) -> Vec<(f32, f32)> {
let mut polyline: Vec<(f32, f32)> = Vec::new();
let mut cx = 0.0f32;
let mut cy = 0.0f32;
for cmd in cmds {
match cmd {
PathCmd::MoveTo(x, y) => {
cx = *x;
cy = *y;
polyline.push((cx, cy));
}
PathCmd::LineTo(x, y) => {
polyline.push((*x, *y));
cx = *x;
cy = *y;
}
PathCmd::CubicBezier(cx1, cy1, cx2, cy2, x, y) => {
let steps = 20;
for i in 1..=steps {
let t = i as f32 / steps as f32;
let (px, py) = cubic_bezier(cx, cy, *cx1, *cy1, *cx2, *cy2, *x, *y, t);
polyline.push((px, py));
}
cx = *x;
cy = *y;
}
PathCmd::QuadBezier(qcx, qcy, x, y) => {
let steps = 15;
for i in 1..=steps {
let t = i as f32 / steps as f32;
let (px, py) = quad_bezier(cx, cy, *qcx, *qcy, *x, *y, t);
polyline.push((px, py));
}
cx = *x;
cy = *y;
}
}
}
if polyline.len() < 2 {
return polyline;
}
let mut lengths = vec![0.0f32];
for i in 1..polyline.len() {
let dx = polyline[i].0 - polyline[i - 1].0;
let dy = polyline[i].1 - polyline[i - 1].1;
lengths.push(lengths[i - 1] + (dx * dx + dy * dy).sqrt());
}
let total = *lengths.last().unwrap();
let mut result = Vec::with_capacity(n);
for i in 0..n {
let target = total * (i as f32 / (n - 1).max(1) as f32);
let seg = lengths
.windows(2)
.position(|w| w[0] <= target && target <= w[1])
.unwrap_or(polyline.len() - 2);
let seg_len = lengths[seg + 1] - lengths[seg];
let t = if seg_len > 1e-6 {
(target - lengths[seg]) / seg_len
} else {
0.0
};
let px = polyline[seg].0 + t * (polyline[seg + 1].0 - polyline[seg].0);
let py = polyline[seg].1 + t * (polyline[seg + 1].1 - polyline[seg].1);
result.push((px, py));
}
result
}
fn cubic_bezier(
x0: f32,
y0: f32,
cx1: f32,
cy1: f32,
cx2: f32,
cy2: f32,
x1: f32,
y1: f32,
t: f32,
) -> (f32, f32) {
let u = 1.0 - t;
let u2 = u * u;
let u3 = u2 * u;
let t2 = t * t;
let t3 = t2 * t;
(
u3 * x0 + 3.0 * u2 * t * cx1 + 3.0 * u * t2 * cx2 + t3 * x1,
u3 * y0 + 3.0 * u2 * t * cy1 + 3.0 * u * t2 * cy2 + t3 * y1,
)
}
fn quad_bezier(x0: f32, y0: f32, cx: f32, cy: f32, x1: f32, y1: f32, t: f32) -> (f32, f32) {
let u = 1.0 - t;
(
u * u * x0 + 2.0 * u * t * cx + t * t * x1,
u * u * y0 + 2.0 * u * t * cy + t * t * y1,
)
}
fn path_length(points: &[(f32, f32)]) -> f32 {
let mut len = 0.0;
for i in 1..points.len() {
let dx = points[i].0 - points[i - 1].0;
let dy = points[i].1 - points[i - 1].1;
len += (dx * dx + dy * dy).sqrt();
}
len
}
pub fn interpolate_profile(profile: &[f32], n: usize) -> Vec<f32> {
if profile.is_empty() {
return vec![0.1; n];
}
if profile.len() == 1 {
return vec![profile[0]; n];
}
let mut result = Vec::with_capacity(n);
for i in 0..n {
let t = i as f32 / (n - 1).max(1) as f32;
let pos = t * (profile.len() - 1) as f32;
let idx = (pos as usize).min(profile.len() - 2);
let frac = pos - idx as f32;
result.push(profile[idx] * (1.0 - frac) + profile[idx + 1] * frac);
}
result
}
fn error_out(msg: &str) -> HashMap<String, Message> {
let mut out = HashMap::new();
out.insert("error".to_string(), Message::Error(msg.to_string().into()));
out
}