1#[derive(Debug, Clone, Copy, PartialEq, Hash)]
5pub enum StrokeJoin {
6 Miter,
7 Bevel,
8}
9
10#[derive(Debug, Clone, Copy, PartialEq, Hash)]
11pub enum StrokeCap {
12 Butt,
13 Square,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Stroke {
18 pub miter_limit: f64,
19 pub thickness: f64,
20 pub join: StrokeJoin,
21 pub cap: StrokeCap,
22}
23
24impl Default for Stroke {
25 fn default() -> Self {
26 Self {
27 miter_limit: 10.0,
28 thickness: 1.0,
29 join: StrokeJoin::Miter,
30 cap: StrokeCap::Butt,
31 }
32 }
33}
34
35#[derive(Debug, Default, Clone, PartialEq)]
36pub struct Mesh {
37 pub positions: Vec<[f64; 2]>,
38 pub indices: Vec<[u32; 3]>,
39}
40
41#[derive(Debug, Copy, Clone, PartialEq)]
42struct StrokeState {
43 last_flip: f64,
44 started: bool,
45 normal: Option<[f64; 2]>,
46}
47
48impl Default for StrokeState {
49 fn default() -> Self {
50 Self {
51 last_flip: -1.0,
52 started: false,
53 normal: None,
54 }
55 }
56}
57
58#[derive(Debug, Copy, Clone, PartialEq)]
59struct SegArgs {
60 index: u32,
61 last: [f64; 2],
62 cur: [f64; 2],
63 next: Option<[f64; 2]>,
64 half_thick: f64,
65}
66
67impl Stroke {
68 pub fn thickness(mut self, thickness: f64) -> Self {
69 self.thickness = thickness;
70 self
71 }
72
73 pub fn miter_limit(mut self, miter_limit: f64) -> Self {
74 self.miter_limit = miter_limit;
75 self
76 }
77
78 pub fn join(mut self, join: StrokeJoin) -> Self {
79 self.join = join;
80 self
81 }
82
83 pub fn cap(mut self, cap: StrokeCap) -> Self {
84 self.cap = cap;
85 self
86 }
87
88 pub fn build(self, points: &[[f64; 2]]) -> Mesh {
89 self.build_with_thickness_fn(points, |_, _, _| self.thickness)
90 }
91
92 pub fn build_with_thickness_fn(
93 self,
94 points: &[[f64; 2]],
95 mut thickness_fn: impl FnMut([f64; 2], usize, &[[f64; 2]]) -> f64,
96 ) -> Mesh {
97 let mut complex = Mesh::default();
98
99 if points.len() <= 1 {
100 return complex;
101 }
102
103 let mut state = StrokeState::default();
104 let mut count = 0;
106 for (i, pt) in points.windows(2).enumerate() {
107 if let [last, current] = pt {
108 let next = points.get(i + 2).copied();
109 let thickness = thickness_fn(*current, i, points);
110 let amt = self.seg(
111 &mut state,
112 &mut complex,
113 SegArgs {
114 index: count,
115 last: *last,
116 cur: *current,
117 next,
118 half_thick: thickness * 0.5,
119 },
120 );
121 count += amt;
122 }
123 }
124
125 complex
126 }
127
128 fn seg(
129 &self,
130 state: &mut StrokeState,
131 complex: &mut Mesh,
132 SegArgs {
133 index,
134 mut last,
135 mut cur,
136 next,
137 half_thick,
138 }: SegArgs,
139 ) -> u32 {
140 let mut count = 0;
141 let cells = &mut complex.indices;
142 let positions = &mut complex.positions;
143 let cap_square = matches!(self.cap, StrokeCap::Square);
144 let join_bevel = matches!(self.join, StrokeJoin::Bevel);
145
146 let line_a = direction(cur, last);
148
149 if state.normal.is_none() {
153 state.normal = Some(normal(line_a));
154 }
155
156 if !state.started {
158 state.started = true;
159
160 if cap_square {
162 last = scale_and_add(last, line_a, -half_thick);
163 }
164 positions.extend_from_slice(&extrusions(last, state.normal.unwrap(), half_thick));
165 }
166
167 cells.push([index, index + 1, index + 2]);
168
169 if let Some(next) = next {
178 let line_b = direction(next, cur);
181
182 let (tangent, miter, miter_len) = compute_miter(line_a, line_b, half_thick);
184
185 let mut flip = if dot(tangent, state.normal.unwrap()) < 0.0 {
187 -1.0
188 } else {
189 1.0
190 };
191
192 let mut bevel = join_bevel;
193 if matches!(self.join, StrokeJoin::Miter) {
194 let limit = miter_len / half_thick;
195 if limit > self.miter_limit {
196 bevel = true;
197 }
198 }
199
200 if bevel {
201 let tmp = scale_and_add(cur, state.normal.unwrap(), -half_thick * flip);
203 positions.push(tmp);
204 let tmp = scale_and_add(cur, miter, miter_len * flip);
205 positions.push(tmp);
206
207 cells.push(if state.last_flip != -flip {
208 [index, index + 2, index + 3]
209 } else {
210 [index + 2, index + 1, index + 3]
211 });
212 cells.push([index + 2, index + 3, index + 4]);
213
214 let tmp = normal(line_b);
215 state.normal = Some(tmp); let tmp = scale_and_add(cur, tmp, -half_thick * flip);
218 positions.push(tmp);
219
220 count += 3;
222 } else {
223 positions.extend_from_slice(&extrusions(cur, miter, miter_len));
226
227 cells.push(if state.last_flip == 1.0 {
228 [index, index + 2, index + 3]
229 } else {
230 [index + 2, index + 1, index + 3]
231 });
232
233 flip = -1.0;
234
235 state.normal = Some(miter);
237 count += 2
238 }
239 state.last_flip = flip;
240 } else {
241 state.normal = Some(normal(line_a));
244
245 if cap_square {
247 cur = scale_and_add(cur, line_a, half_thick);
248 }
249
250 positions.extend_from_slice(&extrusions(cur, state.normal.unwrap(), half_thick));
251 cells.push(if state.last_flip == 1.0 {
252 [index, index + 2, index + 3]
253 } else {
254 [index + 2, index + 1, index + 3]
255 });
256
257 count += 2;
258 }
259 count
260 }
261}
262
263fn extrusions(point: [f64; 2], normal: [f64; 2], scale: f64) -> [[f64; 2]; 2] {
264 [
265 scale_and_add(point, normal, -scale),
266 scale_and_add(point, normal, scale),
267 ]
268}
269
270fn scale_and_add(a: [f64; 2], b: [f64; 2], scale: f64) -> [f64; 2] {
271 [a[0] + b[0] * scale, a[1] + b[1] * scale]
272}
273
274fn subtract(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
275 [a[0] - b[0], a[1] - b[1]]
276}
277
278fn add(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
279 [a[0] + b[0], a[1] + b[1]]
280}
281
282fn normal(dir: [f64; 2]) -> [f64; 2] {
283 [-dir[1], dir[0]]
284}
285
286fn direction(a: [f64; 2], b: [f64; 2]) -> [f64; 2] {
287 normalize(subtract(a, b))
288}
289
290fn compute_miter(line_a: [f64; 2], line_b: [f64; 2], half_thick: f64) -> ([f64; 2], [f64; 2], f64) {
291 let tangent = normalize(add(line_a, line_b));
293
294 let miter = [-tangent[1], tangent[0]];
296 let tmp = [-line_a[1], line_a[0]];
297
298 (tangent, miter, half_thick / dot(miter, tmp))
300}
301
302fn normalize(a: [f64; 2]) -> [f64; 2] {
303 let [x, y] = a;
304 let len = x * x + y * y;
305 let len = if len > 0.0 { 1.0 / len.sqrt() } else { len };
306 [a[0] * len, a[1] * len]
307}
308
309fn dot(a: [f64; 2], b: [f64; 2]) -> f64 {
310 a[0] * b[0] + a[1] * b[1]
311}
312
313#[cfg(test)]
314mod tests {
315 use super::{Mesh, Stroke};
316
317 #[test]
318 fn should_extrude_path_two_points() {
319 let mesh = Stroke::default()
320 .thickness(2.5)
321 .build(&[[2.0, 0.0], [2.0, 10.0]]);
322
323 assert_eq!(
324 mesh,
325 Mesh {
326 positions: vec![[3.25, 0.0], [0.75, 0.0], [3.25, 10.0], [0.75, 10.0]],
327 indices: vec![[0, 1, 2], [2, 1, 3]]
328 }
329 )
330 }
331
332 #[test]
333 fn should_extrude_path_three_points() {
334 let mesh = Stroke::default()
335 .thickness(1.0)
336 .build(&[[0.0, 0.0], [1.0, 1.0], [2.0, 0.0]]);
337
338 assert_eq!(
339 mesh,
340 Mesh {
341 positions: vec![
342 [0.35355339059327373, -0.35355339059327373],
343 [-0.35355339059327373, 0.35355339059327373],
344 [1.0, 0.2928932188134524],
345 [1.0, 1.7071067811865475],
346 [1.6464466094067263, -0.35355339059327373],
347 [2.353553390593274, 0.35355339059327373]
348 ],
349 indices: vec![[0, 1, 2], [2, 1, 3], [2, 3, 4], [4, 3, 5]]
350 }
351 )
352 }
353}