lumen_engine/node/vector/
path.rs1use super::paint::Paint;
2use crate::gpu::{BoundFrame, CompiledOutput, FrameBindContext, GpuCompileNode, GpuCompiledNode};
3use crate::node::{NodeId, NodeParamEvalContext, NodeParams, PortRef};
4
5#[derive(Debug, Clone, lumen_macros::Delegate)]
7pub struct PathParams {
8 #[meta(
10 name = "Path data",
11 format = "path_data",
12 multiline,
13 recommended_rows = 5
14 )]
15 pub data: String,
16 #[meta()]
18 pub position: (f64, f64),
19 #[meta()]
21 pub fill_enabled: bool,
22 #[meta()]
24 pub fill_paint: Paint,
25 #[meta()]
27 pub stroke_enabled: bool,
28 #[meta()]
30 pub stroke_paint: Paint,
31 #[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#[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(¶ms));
119 if !points.is_empty() {
120 bound.write_buffer(self.points_buffer, 0, bytemuck::cast_slice(&points));
121 }
122 Ok(())
123 }
124}
125
126fn 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}