reflow_components 0.2.1

Standard component catalog for Reflow — procedural, media, GPU, animation, I/O, and stream actors.
Documentation
//! Procedural tube mesh generator — creates a mesh along a Bezier curve.
//!
//! Bypasses SDF + MarchingCubes entirely. Directly generates triangle mesh
//! from cross-section circles along a curve. Zero artifacts by construction.
//!
//! Config:
//!   path: SVG-like path string (M, L, C, Q commands)
//!   profile: radius at evenly spaced points (interpolated)
//!   segments: number of cross-sections along the curve
//!   rings: number of vertices per cross-section circle (default 16)
//!   plane: "xz" | "xy" | "yz"

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;

// Reuse the path parsing from SdfPathActor
use crate::gpu::sdf::path::{interpolate_profile, parse_path, sample_path};

#[actor(
    TubeMeshActor,
    inports::<1>(),
    outports::<1>(mesh, uv, metadata),
    state(MemoryState)
)]
pub async fn tube_mesh_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(32) as usize;
    let rings = config.get("rings").and_then(|v| v.as_u64()).unwrap_or(16) as usize;
    let plane = config.get("plane").and_then(|v| v.as_str()).unwrap_or("xz");
    // Head shaping: headLength = fraction of tube that's the "head" (0-1),
    // headFlatten = vertical squash factor in head region (0=flat, 1=round)
    let head_length = config
        .get("headLength")
        .and_then(|v| v.as_f64())
        .unwrap_or(0.0) as f32;
    let head_flatten = config
        .get("headFlatten")
        .and_then(|v| v.as_f64())
        .unwrap_or(1.0) as f32;

    // Parse and sample path
    let commands = parse_path(path_str);
    let points_2d = sample_path(&commands, segments);
    if points_2d.len() < 2 {
        return Err(anyhow::anyhow!("Path needs at least 2 points"));
    }

    let radii = interpolate_profile(&profile, points_2d.len());

    // Convert 2D points to 3D
    let points: Vec<[f32; 3]> = points_2d
        .iter()
        .map(|(x, y)| {
            match plane {
                "xy" => [*x, *y, 0.0],
                "yz" => [0.0, *x, *y],
                _ => [*x, 0.0, *y], // xz
            }
        })
        .collect();

    // Generate tube mesh
    let n = points.len();
    let _vertex_count = n * rings;
    let triangle_count = (n - 1) * rings * 2;
    let stride = 24; // pos3 + normal3

    let mut mesh_data = Vec::with_capacity(triangle_count * 3 * stride);
    let mut uv_data: Vec<u8> = Vec::with_capacity(triangle_count * 3 * 8); // 2 floats per vertex

    // Compute per-point tangent, normal, binormal (Frenet frame)
    let mut tangents: Vec<[f32; 3]> = Vec::with_capacity(n);
    for i in 0..n {
        let t = if i == 0 {
            sub3(points[1], points[0])
        } else if i == n - 1 {
            sub3(points[n - 1], points[n - 2])
        } else {
            sub3(points[i + 1], points[i - 1])
        };
        tangents.push(normalize3(t));
    }

    // Build cross-section circles using rotation-minimizing frame
    let mut normals: Vec<[f32; 3]> = Vec::with_capacity(n);
    let mut binormals: Vec<[f32; 3]> = Vec::with_capacity(n);

    // Initial frame: find a vector not parallel to the first tangent
    let t0 = tangents[0];
    let up = if t0[1].abs() < 0.9 {
        [0.0, 1.0, 0.0]
    } else {
        [1.0, 0.0, 0.0]
    };
    let b0 = normalize3(cross3(t0, up));
    let n0 = cross3(b0, t0);
    normals.push(n0);
    binormals.push(b0);

    // Propagate frame along curve (rotation minimizing)
    for i in 1..n {
        let t_prev = tangents[i - 1];
        let t_curr = tangents[i];

        // Reflect previous frame across the tangent bisector
        let v1 = sub3(points[i], points[i - 1]);
        let c1 = dot3(v1, v1);
        if c1 < 1e-10 {
            normals.push(normals[i - 1]);
            binormals.push(binormals[i - 1]);
            continue;
        }

        let n_prev = normals[i - 1];
        let b_prev = binormals[i - 1];

        // Double reflection method for rotation minimizing frames
        let r_l = sub3(n_prev, scale3(v1, 2.0 * dot3(v1, n_prev) / c1));
        let r_b = sub3(b_prev, scale3(v1, 2.0 * dot3(v1, b_prev) / c1));

        let v2 = sub3(
            t_curr,
            sub3(t_prev, scale3(v1, 2.0 * dot3(v1, t_prev) / c1)),
        );
        let c2 = dot3(v2, v2);
        if c2 < 1e-10 {
            normals.push(normalize3(r_l));
            binormals.push(normalize3(r_b));
            continue;
        }

        let new_n = sub3(r_l, scale3(v2, 2.0 * dot3(v2, r_l) / c2));
        let new_b = sub3(r_b, scale3(v2, 2.0 * dot3(v2, r_b) / c2));
        normals.push(normalize3(new_n));
        binormals.push(normalize3(new_b));
    }

    // Generate circle vertices at each cross-section: (pos, normal, u, v)
    let mut circle_verts: Vec<Vec<([f32; 3], [f32; 3], f32, f32)>> = Vec::with_capacity(n);
    for i in 0..n {
        let center = points[i];
        let r = radii[i];
        let nor = normals[i];
        let bin = binormals[i];

        // Head flattening: squash the normal axis in the head region
        let t = i as f32 / (n - 1).max(1) as f32; // 0 = head, 1 = tail
        let flatten = if head_length > 0.0 && t < head_length {
            let head_t = t / head_length; // 0 at tip, 1 at neck
                                          // Smooth transition from flattened to round
            head_flatten + (1.0 - head_flatten) * head_t * head_t
        } else {
            1.0
        };

        let mut ring_verts = Vec::with_capacity(rings);
        for j in 0..rings {
            let angle = 2.0 * std::f32::consts::PI * j as f32 / rings as f32;
            let cos_a = angle.cos();
            let sin_a = angle.sin();

            // Position on ellipse (flatten normal axis for head)
            let offset = add3(scale3(nor, cos_a * r * flatten), scale3(bin, sin_a * r));
            let pos = add3(center, offset);

            // Normal accounts for flattening
            let nor_scaled = add3(scale3(nor, cos_a * flatten), scale3(bin, sin_a));
            let normal = normalize3(nor_scaled);

            // UV: u = position along curve, v = position around ring
            let u = i as f32 / (n - 1).max(1) as f32;
            let v = j as f32 / rings as f32;

            ring_verts.push((pos, normal, u, v));
        }
        circle_verts.push(ring_verts);
    }

    // Generate triangles connecting adjacent rings
    for i in 0..n - 1 {
        let ring_a = &circle_verts[i];
        let ring_b = &circle_verts[i + 1];

        for j in 0..rings {
            let j_next = (j + 1) % rings;

            let (pa, na, ua, va) = ring_a[j];
            let (pb, nb, ub, vb) = ring_b[j];
            let (pc, nc, uc, vc) = ring_b[j_next];
            let (pd, nd, ud, vd) = ring_a[j_next];

            // Triangle 1: a, b, c
            emit_vertex(&mut mesh_data, pa, na);
            emit_uv(&mut uv_data, ua, va);
            emit_vertex(&mut mesh_data, pb, nb);
            emit_uv(&mut uv_data, ub, vb);
            emit_vertex(&mut mesh_data, pc, nc);
            emit_uv(&mut uv_data, uc, vc);

            // Triangle 2: a, c, d
            emit_vertex(&mut mesh_data, pa, na);
            emit_uv(&mut uv_data, ua, va);
            emit_vertex(&mut mesh_data, pc, nc);
            emit_uv(&mut uv_data, uc, vc);
            emit_vertex(&mut mesh_data, pd, nd);
            emit_uv(&mut uv_data, ud, vd);
        }
    }

    // Start cap: rounded head (dome along negative tangent direction)
    {
        let head_rings = 6; // number of dome rings
        let head_r = radii[0];
        let head_dir = scale3(tangents[0], -1.0); // forward direction (away from body)
        let head_nor = normals[0];
        let head_bin = binormals[0];
        let head_center = points[0];

        let head_flatten_val = if head_length > 0.0 { head_flatten } else { 1.0 };

        let mut prev_ring = circle_verts[0].clone();

        for hi in 1..=head_rings {
            let t = hi as f32 / head_rings as f32; // 0 at first ring, 1 at tip
            let phi = t * std::f32::consts::FRAC_PI_2; // 0 to 90 degrees
            let ring_r = head_r * phi.cos(); // shrinks to 0 at tip
            let forward = head_r * phi.sin() * 1.2; // how far forward

            let ring_center = add3(head_center, scale3(head_dir, forward));

            let mut curr_ring = Vec::with_capacity(rings);
            for j in 0..rings {
                let angle = 2.0 * std::f32::consts::PI * j as f32 / rings as f32;
                let cos_a = angle.cos();
                let sin_a = angle.sin();

                let flatten = head_flatten_val + (1.0 - head_flatten_val) * (1.0 - t);
                let offset = add3(
                    scale3(head_nor, cos_a * ring_r * flatten),
                    scale3(head_bin, sin_a * ring_r),
                );
                let pos = add3(ring_center, offset);
                let normal = normalize3(add3(offset, scale3(head_dir, ring_r * 0.5)));
                let u_head = -t * 0.1; // negative u for head region
                let v_ring = j as f32 / rings as f32;
                curr_ring.push((pos, normal, u_head, v_ring));
            }

            // Connect prev_ring to curr_ring
            for j in 0..rings {
                let j_next = (j + 1) % rings;
                let (pa, na, ua, va) = prev_ring[j];
                let (pb, nb, ub, vb) = curr_ring[j];
                let (pc, nc, uc, vc) = curr_ring[j_next];
                let (pd, nd, ud, vd) = prev_ring[j_next];

                emit_vertex(&mut mesh_data, pa, na);
                emit_uv(&mut uv_data, ua, va);
                emit_vertex(&mut mesh_data, pb, nb);
                emit_uv(&mut uv_data, ub, vb);
                emit_vertex(&mut mesh_data, pc, nc);
                emit_uv(&mut uv_data, uc, vc);

                emit_vertex(&mut mesh_data, pa, na);
                emit_uv(&mut uv_data, ua, va);
                emit_vertex(&mut mesh_data, pc, nc);
                emit_uv(&mut uv_data, uc, vc);
                emit_vertex(&mut mesh_data, pd, nd);
                emit_uv(&mut uv_data, ud, vd);
            }

            prev_ring = curr_ring;
        }

        // Close the tip with a fan
        let tip = add3(head_center, scale3(head_dir, head_r * 1.2));
        let tip_nor = head_dir;
        for j in 0..rings {
            let j_next = (j + 1) % rings;
            emit_vertex(&mut mesh_data, tip, tip_nor);
            emit_uv(&mut uv_data, -0.1, 0.5);
            emit_vertex(&mut mesh_data, prev_ring[j_next].0, prev_ring[j_next].1);
            emit_uv(&mut uv_data, prev_ring[j_next].2, prev_ring[j_next].3);
            emit_vertex(&mut mesh_data, prev_ring[j].0, prev_ring[j].1);
            emit_uv(&mut uv_data, prev_ring[j].2, prev_ring[j].3);
        }
    }
    // End cap (tail)
    {
        let center = points[n - 1];
        let nor = tangents[n - 1];
        for j in 0..rings {
            let j_next = (j + 1) % rings;
            emit_vertex(&mut mesh_data, center, nor);
            emit_uv(&mut uv_data, 1.0, 0.5);
            emit_vertex(&mut mesh_data, circle_verts[n - 1][j].0, nor);
            emit_uv(
                &mut uv_data,
                circle_verts[n - 1][j].2,
                circle_verts[n - 1][j].3,
            );
            emit_vertex(&mut mesh_data, circle_verts[n - 1][j_next].0, nor);
            emit_uv(
                &mut uv_data,
                circle_verts[n - 1][j_next].2,
                circle_verts[n - 1][j_next].3,
            );
        }
    }

    let total_verts = mesh_data.len() / stride;

    let mut out = HashMap::new();
    out.insert("mesh".to_string(), Message::bytes(mesh_data));
    out.insert("uv".to_string(), Message::bytes(uv_data));
    out.insert(
        "metadata".to_string(),
        Message::object(EncodableValue::from(json!({
            "vertexCount": total_verts,
            "triangleCount": total_verts / 3,
            "segments": n,
            "rings": rings,
            "stride": stride,
            "format": "pos3_normal3_f32",
        }))),
    );
    Ok(out)
}

fn emit_uv(buf: &mut Vec<u8>, u: f32, v: f32) {
    buf.extend_from_slice(&u.to_le_bytes());
    buf.extend_from_slice(&v.to_le_bytes());
}

fn emit_vertex(buf: &mut Vec<u8>, pos: [f32; 3], nor: [f32; 3]) {
    for f in &pos {
        buf.extend_from_slice(&f.to_le_bytes());
    }
    for f in &nor {
        buf.extend_from_slice(&f.to_le_bytes());
    }
}

fn add3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[0] + b[0], a[1] + b[1], a[2] + b[2]]
}
fn sub3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [a[0] - b[0], a[1] - b[1], a[2] - b[2]]
}
fn scale3(v: [f32; 3], s: f32) -> [f32; 3] {
    [v[0] * s, v[1] * s, v[2] * s]
}
fn dot3(a: [f32; 3], b: [f32; 3]) -> f32 {
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
fn cross3(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
    [
        a[1] * b[2] - a[2] * b[1],
        a[2] * b[0] - a[0] * b[2],
        a[0] * b[1] - a[1] * b[0],
    ]
}
fn normalize3(v: [f32; 3]) -> [f32; 3] {
    let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
    if l > 1e-8 {
        [v[0] / l, v[1] / l, v[2] / l]
    } else {
        [0.0, 1.0, 0.0]
    }
}