Skip to main content

lumen_engine/node/vector/
path.rs

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