Skip to main content

lumen_engine/node/vector/
path.rs

1use super::paint::Paint;
2use crate::gpu::{BoundFrame, CompiledOutput, FrameBindContext, GpuCompileNode, GpuCompiledNode};
3use crate::node::{NodeId, NodeParamEvalContext, NodeParams, PortRef};
4
5/// Produces a rasterized vector path source.
6#[derive(Debug, Clone, lumen_macros::Delegate)]
7pub struct PathParams {
8    /// SVG-style path data.
9    #[meta(
10        name = "Path data",
11        format = "path_data",
12        multiline,
13        recommended_rows = 5
14    )]
15    pub data: String,
16    /// Path origin in pixels.
17    #[meta()]
18    pub position: (f64, f64),
19    /// Enables fill rendering.
20    #[meta()]
21    pub fill_enabled: bool,
22    /// Fill paint. Accepts a solid color or gradient.
23    #[meta()]
24    pub fill_paint: Paint,
25    /// Enables stroke rendering.
26    #[meta()]
27    pub stroke_enabled: bool,
28    /// Stroke paint. Accepts a solid color or gradient.
29    #[meta()]
30    pub stroke_paint: Paint,
31    /// Stroke width in pixels.
32    #[meta(min = 0, step = 0.5)]
33    pub stroke_width: f64,
34}
35
36impl Default for PathParams {
37    fn default() -> Self {
38        Self {
39            data: "M 0 0 L 100 0 L 100 100 L 0 100 Z".to_string(),
40            position: (0.0, 0.0),
41            fill_enabled: true,
42            fill_paint: Paint::solid([255, 255, 255, 255]),
43            stroke_enabled: false,
44            stroke_paint: Paint::solid([0, 0, 0, 255]),
45            stroke_width: 1.0,
46        }
47    }
48}
49
50/// Produces a rasterized vector path source.
51#[derive(Debug, Clone, lumen_macros::Node)]
52#[node(kind = "path", name = "Path", category = "vector")]
53pub struct Path {
54    pub id: NodeId,
55    #[params]
56    pub params: PathParamsDelegate,
57}
58
59impl Default for Path {
60    fn default() -> Self {
61        Self {
62            id: NodeId::new(0),
63            params: PathParamsDelegate::default(),
64        }
65    }
66}
67
68impl GpuCompileNode for Path {
69    fn compile_gpu(
70        &self,
71        ctx: &mut crate::gpu::CompileContext<'_>,
72        port: &PortRef,
73    ) -> crate::Result<CompiledOutput> {
74        crate::node::vector::renderer::VectorRenderer::new(ctx).compile_path(self, port)
75    }
76}
77
78#[derive(Debug, Clone)]
79pub(crate) struct CompiledPath {
80    pub(crate) node_id: NodeId,
81    pub(crate) params: PathParamsDelegate,
82    pub(crate) params_buffer: lumen_gpu::BufferId,
83    pub(crate) points_buffer: lumen_gpu::BufferId,
84    pub(crate) max_points: usize,
85}
86
87impl GpuCompiledNode for CompiledPath {
88    fn node_id(&self) -> NodeId {
89        self.node_id
90    }
91
92    fn bind(&self, ctx: &FrameBindContext<'_>, bound: &mut BoundFrame) -> crate::Result<()> {
93        let evaluated = self.params.eval(&NodeParamEvalContext {
94            node_id: self.node_id,
95            expr: &ctx.expr_context(self.node_id, "params"),
96        })?;
97        let points = parse_path_points(&evaluated.data, self.max_points);
98        let (x, y) = evaluated.position;
99        let mut flags = 0;
100        if evaluated.fill_enabled {
101            flags |= 1;
102        }
103        if evaluated.stroke_enabled {
104            flags |= 2;
105        }
106
107        let params = super::renderer::PathParams {
108            fill_paint: evaluated.fill_paint.to_gpu([255, 255, 255, 255]),
109            stroke_paint: evaluated.stroke_paint.to_gpu([0, 0, 0, 255]),
110            position: [x as f32, y as f32],
111            bounds_min: bounds_min(&points),
112            bounds_size: bounds_size(&points),
113            stroke_width: evaluated.stroke_width as f32,
114            flags,
115            point_count: points.len() as u32,
116            _pad: [0; 3],
117        };
118        bound.write_buffer(self.params_buffer, 0, bytemuck::bytes_of(&params));
119        if !points.is_empty() {
120            bound.write_buffer(self.points_buffer, 0, bytemuck::cast_slice(&points));
121        }
122        Ok(())
123    }
124}
125
126// Pragmatic fallback: this accepts SVG-like path strings by flattening every
127// numeric coordinate pair into a polygon. Curves/arcs are treated as straight
128// point chains until a full path tessellator lands here.
129fn parse_path_points(data: &str, max_points: usize) -> Vec<super::renderer::PathPoint> {
130    let mut numbers = Vec::new();
131    let mut token = String::new();
132    let mut previous = '\0';
133
134    for ch in data.chars() {
135        let starts_exponent = matches!(previous, 'e' | 'E') && matches!(ch, '+' | '-');
136        if matches!(ch, '-' | '+') && !starts_exponent {
137            if !token.is_empty() {
138                if let Ok(value) = token.parse::<f32>() {
139                    numbers.push(value);
140                }
141                token.clear();
142            }
143            token.push(ch);
144        } else if ch.is_ascii_digit() || ch == '.' || starts_exponent || matches!(ch, 'e' | 'E') {
145            token.push(ch);
146        } else if !token.is_empty() {
147            if let Ok(value) = token.parse::<f32>() {
148                numbers.push(value);
149            }
150            token.clear();
151        }
152        previous = ch;
153    }
154    if !token.is_empty()
155        && let Ok(value) = token.parse::<f32>()
156    {
157        numbers.push(value);
158    }
159
160    numbers
161        .chunks_exact(2)
162        .take(max_points)
163        .map(|pair| super::renderer::PathPoint {
164            position: [pair[0], pair[1]],
165        })
166        .collect()
167}
168
169fn bounds_min(points: &[super::renderer::PathPoint]) -> [f32; 2] {
170    let Some(first) = points.first() else {
171        return [0.0, 0.0];
172    };
173    points.iter().skip(1).fold(first.position, |acc, point| {
174        [acc[0].min(point.position[0]), acc[1].min(point.position[1])]
175    })
176}
177
178fn bounds_size(points: &[super::renderer::PathPoint]) -> [f32; 2] {
179    let min = bounds_min(points);
180    let max = points.iter().fold(min, |acc, point| {
181        [acc[0].max(point.position[0]), acc[1].max(point.position[1])]
182    });
183    [(max[0] - min[0]).max(1.0), (max[1] - min[1]).max(1.0)]
184}