lumen_engine/node/vector/
path.rs1use crate::gpu::{
2 BoundFrame, CompiledOutput, FrameBindContext, FrameBinding, GpuCompileNode, GpuFrameBindNode,
3};
4use crate::node::{NodeId, NodeProperty, PortRef};
5
6#[derive(Debug, Clone, lumen_macros::Node)]
8#[node(kind = "path", name = "Path", category = "vector")]
9pub struct Path {
10 pub id: NodeId,
11 #[property(
13 kind = "string",
14 name = "Path data",
15 format = "path_data",
16 multiline,
17 recommended_rows = 5
18 )]
19 pub data: NodeProperty,
20 #[property(kind = "vec2")]
22 pub position: NodeProperty,
23 #[property(kind = "bool")]
25 pub fill_enabled: NodeProperty,
26 #[property(kind = "color")]
28 pub fill_color: NodeProperty,
29 #[property(kind = "bool")]
31 pub stroke_enabled: NodeProperty,
32 #[property(kind = "color")]
34 pub stroke_color: NodeProperty,
35 #[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(¶ms));
137 if !points.is_empty() {
138 bound.write_buffer(*points_buffer, 0, bytemuck::cast_slice(&points));
139 }
140 Ok(())
141 }
142}
143
144fn 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}