use super::paint::Paint;
use crate::gpu::{BoundFrame, CompiledOutput, FrameBindContext, GpuCompileNode, GpuCompiledNode};
use crate::node::{NodeId, NodeParamEvalContext, NodeParams, PortRef};
#[derive(Debug, Clone, lumen_macros::Delegate)]
pub struct PathParams {
#[meta(
name = "Path data",
format = "path_data",
multiline,
recommended_rows = 5
)]
pub data: String,
#[meta()]
pub position: (f64, f64),
#[meta()]
pub fill_enabled: bool,
#[meta()]
pub fill_paint: Paint,
#[meta()]
pub stroke_enabled: bool,
#[meta()]
pub stroke_paint: Paint,
#[meta(min = 0, step = 0.5)]
pub stroke_width: f64,
}
impl Default for PathParams {
fn default() -> Self {
Self {
data: "M 0 0 L 100 0 L 100 100 L 0 100 Z".to_string(),
position: (0.0, 0.0),
fill_enabled: true,
fill_paint: Paint::solid([255, 255, 255, 255]),
stroke_enabled: false,
stroke_paint: Paint::solid([0, 0, 0, 255]),
stroke_width: 1.0,
}
}
}
#[derive(Debug, Clone, lumen_macros::Node)]
#[node(kind = "path", name = "Path", category = "vector")]
pub struct Path {
pub id: NodeId,
#[params]
pub params: PathParamsDelegate,
}
impl Default for Path {
fn default() -> Self {
Self {
id: NodeId::new(0),
params: PathParamsDelegate::default(),
}
}
}
impl GpuCompileNode for Path {
fn compile_gpu(
&self,
ctx: &mut crate::gpu::CompileContext<'_>,
port: &PortRef,
) -> crate::Result<CompiledOutput> {
crate::node::vector::renderer::VectorRenderer::new(ctx).compile_path(self, port)
}
}
#[derive(Debug, Clone)]
pub(crate) struct CompiledPath {
pub(crate) node_id: NodeId,
pub(crate) params: PathParamsDelegate,
pub(crate) params_buffer: lumen_gpu::BufferId,
pub(crate) points_buffer: lumen_gpu::BufferId,
pub(crate) max_points: usize,
}
impl GpuCompiledNode for CompiledPath {
fn node_id(&self) -> NodeId {
self.node_id
}
fn bind(&self, ctx: &FrameBindContext<'_>, bound: &mut BoundFrame) -> crate::Result<()> {
let evaluated = self.params.eval(&NodeParamEvalContext {
node_id: self.node_id,
expr: &ctx.expr_context(self.node_id, "params"),
})?;
let points = parse_path_points(&evaluated.data, self.max_points);
let (x, y) = evaluated.position;
let mut flags = 0;
if evaluated.fill_enabled {
flags |= 1;
}
if evaluated.stroke_enabled {
flags |= 2;
}
let params = super::renderer::PathParams {
fill_paint: evaluated.fill_paint.to_gpu([255, 255, 255, 255]),
stroke_paint: evaluated.stroke_paint.to_gpu([0, 0, 0, 255]),
position: [x as f32, y as f32],
bounds_min: bounds_min(&points),
bounds_size: bounds_size(&points),
stroke_width: evaluated.stroke_width as f32,
flags,
point_count: points.len() as u32,
_pad: [0; 3],
};
bound.write_buffer(self.params_buffer, 0, bytemuck::bytes_of(¶ms));
if !points.is_empty() {
bound.write_buffer(self.points_buffer, 0, bytemuck::cast_slice(&points));
}
Ok(())
}
}
fn parse_path_points(data: &str, max_points: usize) -> Vec<super::renderer::PathPoint> {
let mut numbers = Vec::new();
let mut token = String::new();
let mut previous = '\0';
for ch in data.chars() {
let starts_exponent = matches!(previous, 'e' | 'E') && matches!(ch, '+' | '-');
if matches!(ch, '-' | '+') && !starts_exponent {
if !token.is_empty() {
if let Ok(value) = token.parse::<f32>() {
numbers.push(value);
}
token.clear();
}
token.push(ch);
} else if ch.is_ascii_digit() || ch == '.' || starts_exponent || matches!(ch, 'e' | 'E') {
token.push(ch);
} else if !token.is_empty() {
if let Ok(value) = token.parse::<f32>() {
numbers.push(value);
}
token.clear();
}
previous = ch;
}
if !token.is_empty()
&& let Ok(value) = token.parse::<f32>()
{
numbers.push(value);
}
numbers
.chunks_exact(2)
.take(max_points)
.map(|pair| super::renderer::PathPoint {
position: [pair[0], pair[1]],
})
.collect()
}
fn bounds_min(points: &[super::renderer::PathPoint]) -> [f32; 2] {
let Some(first) = points.first() else {
return [0.0, 0.0];
};
points.iter().skip(1).fold(first.position, |acc, point| {
[acc[0].min(point.position[0]), acc[1].min(point.position[1])]
})
}
fn bounds_size(points: &[super::renderer::PathPoint]) -> [f32; 2] {
let min = bounds_min(points);
let max = points.iter().fold(min, |acc, point| {
[acc[0].max(point.position[0]), acc[1].max(point.position[1])]
});
[(max[0] - min[0]).max(1.0), (max[1] - min[1]).max(1.0)]
}