flo_curves/bezier/path/algorithms/
fill_concave.rs

1use super::fill_convex::*;
2use super::fill_settings::*;
3
4use crate::geo::*;
5use crate::line::*;
6use crate::bezier::*;
7use crate::bezier::path::*;
8
9use std::f64;
10
11///
12/// Item returned from a ray cast intersection by the concave raycasting function (where we can hit an existing edge)
13///
14#[derive(Clone, Debug)]
15enum ConcaveItem<Item> {
16    /// Edge returned by the main raycasting function
17    Edge(Item),
18
19    /// Intersection with an edge detected in an earlier raycasting operation
20    SelfIntersection(usize)
21}
22
23impl<Item> Into<Option<Item>> for ConcaveItem<Item> {
24    fn into(self) -> Option<Item> {
25        match self {
26            ConcaveItem::Edge(item)             => Some(item),
27            ConcaveItem::SelfIntersection(_)    => None
28        }
29    }
30}
31
32///
33/// Represents a long edge that we want to raycast from
34///
35struct LongEdge<Coord> {
36    start:          Coord,
37    end:            Coord,
38    edge_index:     (usize, usize),
39    ray_collided:   bool
40}
41
42///
43/// Retrieves the 'long' edges from a set of edges returned by a raycast tracing operation
44///
45fn find_long_edges<Coord, Item>(edges: &[RayCollision<Coord, Item>], edge_min_len_squared: f64) -> Vec<LongEdge<Coord>>
46where 
47    Coord: Coordinate+Coordinate2D,
48{
49    // Find the edges where we need to cast extra rays
50    let mut long_edges      = vec![];
51    for edge_num in 0..edges.len() {
52        // Get the length of this edge
53        let last_edge               = if edge_num == 0 {
54            edges.len() - 1
55        } else {
56            edge_num-1
57        };
58
59        let edge_offset             = edges[last_edge].position - edges[edge_num].position;
60        let edge_distance_squared   = edge_offset.dot(&edge_offset);
61
62        // Add to the list of long edges if it's long enough to need further ray-casting
63        if edge_distance_squared >= edge_min_len_squared {
64            long_edges.push(LongEdge { 
65                start:          edges[last_edge].position,
66                end:            edges[edge_num].position,
67                edge_index:     (last_edge, edge_num),
68                ray_collided:   false
69            })
70        }
71    }
72
73    long_edges
74}
75
76///
77/// Determines if the 'to' position is further away from the 'center' position than the 'from' position
78///
79fn ray_is_moving_outwards<Coord>(center: &Coord, from: &Coord, to: &Coord) -> bool
80where 
81    Coord: Coordinate+Coordinate2D,
82{
83    // Determine where the 'to' point is along this ray
84    let ray = (*center, *from);
85    let pos = ray.pos_for_point(to);
86
87    // Position will be > 1.0 if the 'to' position is further away that 'from'
88    pos > 1.0
89}
90
91///
92/// Smooths out small gaps found in a list of edges/long edges
93///
94/// When we encounter a corner or a gap, some rays will leave to find the other side. We can work out how large the
95/// gap the ray escaped through by looking at the long edges in pairs and checking the start and end point of each edge.
96/// If they're closer than the minimum size, we can remove the edge by moving all the points that were found on the other
97/// side into a line.
98///
99fn remove_small_gaps<Coord, Item>(center: &Coord, edges: &mut Vec<RayCollision<Coord, Item>>, long_edges: &mut Vec<LongEdge<Coord>>, min_gap_size: f64)
100where
101    Coord: Coordinate+Coordinate2D,
102{
103    // To avoid calculating a lot of square roots, square the min gap size
104    let min_gap_sq          = min_gap_size * min_gap_size;
105
106    // List of long edges to remove after we've edited the points
107    let mut long_edges_to_remove = vec![];
108
109    // Inspect the 'long edges' as pairs (they need to be in order for this to work)
110    for edge1_idx in 0..long_edges.len() {
111        // Going to measure the distance between this edge and the following one
112        let edge2_idx   = if edge1_idx < long_edges.len()-1 { edge1_idx + 1 } else { 0 };
113        let edge1       = &long_edges[edge1_idx];
114        let edge2       = &long_edges[edge2_idx];
115
116        // Edge1 must be moving out from the center
117        if ray_is_moving_outwards(center, &edge1.start, &edge1.end) && !ray_is_moving_outwards(center, &edge2.start, &edge2.end) {
118            // Work out the gap between the start and the end of this gap
119            let start_pos   = &edge1.start;
120            let end_pos     = &edge2.end;
121            let offset      = *end_pos - *start_pos;
122            let distance_sq = offset.dot(&offset);
123
124            // If it's less than the min gap size, add it to the list of edges to remove
125            if distance_sq <= min_gap_sq {
126                // Move all the points between the two 'long' edges onto a line between the start and end point
127                // Alternatively: could remove the points here to produce a smoother shape later on
128                let gap_line        = (edge1.start, edge2.end);
129                let mut edge_num    = edge1.edge_index.1;
130
131                loop {
132                    // Stop once we reach the end of the final edge
133                    if edge_num == edge2.edge_index.1 {
134                        break;
135                    }
136
137                    // Map this edge to the gap line
138                    let edge        = &mut edges[edge_num];
139                    let edge_ray    = (*center, edge.position);
140                    edge.position   = line_intersects_ray(&edge_ray, &gap_line).unwrap_or(edge.position);
141
142                    // Move to the next edge
143                    edge_num += 1;
144                    if edge_num >= edges.len() { edge_num = 0; }
145                }
146
147                // Remove these edges from consideration for future raycast operations
148                long_edges_to_remove.push(edge1_idx);
149                long_edges_to_remove.push(edge2_idx);
150            }
151        }
152    }
153
154    // Remove any long edges that were affected by the gap removal operation
155    if !long_edges_to_remove.is_empty() {
156        // Unstable sort is OK because this uses primitive types
157        long_edges_to_remove.sort_unstable();
158        for long_edge_num in long_edges_to_remove.into_iter().rev() {
159            long_edges.remove(long_edge_num);
160        }
161    }
162}
163
164///
165/// Traces the outline of a complex area using ray-casting
166///
167/// While the convex version of this function can only trace the outline of a region as it can be reached by a single ray, this
168/// concave version can trace outlines with edges that are not in 'line of sight' from the origin point. The extra work required
169/// for this can be quite time-consuming, so an efficient ray-casting function is vital if the path is particularly complex.
170///
171/// The current version of the algorithm works by taking the result from a convex ray-cast and finding large gaps where no points
172/// were detected, and filling them in by ray-casting from there. There are cases where the resulting path can overlap itself: after
173/// fitting the curve, use `remove_interior_points` to generate a non-overlapping path.
174///
175/// Collisions generated internally will have 'None' set for the `what` field of the ray collision (this is why the field is made
176/// optional by this call)
177///
178pub fn trace_outline_concave<Coord, Item, RayList, RayFn>(center: Coord, options: &FillSettings, cast_ray: RayFn) -> Vec<RayCollision<Coord, Option<Item>>> 
179where
180    Coord:      Coordinate+Coordinate2D,
181    RayList:    IntoIterator<Item=RayCollision<Coord, Item>>,
182    RayFn:      Fn(Coord, Coord) -> RayList,
183{
184    // Modify the raycasting function to return concave items (so we can distinguish between edges we introduced and ones matched by the original raycasting algorithm)
185    // TODO: this just ensures we return optional items
186    let cast_ray                    = &cast_ray;
187    let cast_ray                    = &|from, to| {
188        cast_ray(from, to).into_iter().map(|collision| {
189            RayCollision {
190                position:   collision.position,
191                what:       ConcaveItem::Edge(collision.what)
192            }
193        })
194    };
195
196    // The edge min length is the length of edge we need to see before we'll 'look around' a corner
197    let edge_min_len                = options.step * 4.0;
198    let edge_min_len_squared        = edge_min_len * edge_min_len;
199
200    // Distance to move past a self-intersection (so we fully close the path). This can be reasonably large (as we'll use the
201    // edge from the ray casting function if it's nearer)
202    let self_intersection_distance  = options.step;
203
204    // Perform the initial convex ray-casting
205    let mut edges = trace_outline_convex(center, options, cast_ray);
206
207    // Stop if we found no collisions
208    if edges.len() < 2 {
209        return vec![];
210    }
211
212    // Find the edges where we need to cast extra rays
213    let mut long_edges      = find_long_edges(&edges, edge_min_len_squared);
214
215    // Remove any gaps that are too small for the rays to escape through
216    if let Some(min_gap) = options.min_gap {
217        remove_small_gaps(&center, &mut edges, &mut long_edges, min_gap);
218    }
219
220    // TODO: cast rays from each of the 'long' edges and update the edge list
221    let mut long_edge_index = 0;
222    while long_edge_index < long_edges.len() {
223        // Fetch the next edge to cast from
224        let next_edge = &long_edges[long_edge_index];
225
226        // Skip edges where we've already self-intersected
227        if !next_edge.ray_collided {
228            // Pick the center point
229            let center_point = (next_edge.start + next_edge.end) * 0.5;
230            let offset      = next_edge.start - next_edge.end;
231
232            // Find the angle of the next edge
233            let line_angle  = offset.x().atan2(offset.y());
234
235            // Generate a version of the raycasting function that inspects the existing list of long edges
236            let cast_ray_to_edges   = |from: Coord, to: Coord| {
237                // Generate the edge collisions from the main raycasting function
238                let edge_collisions     = cast_ray(from, to);
239                let ray_line            = (from, to);
240
241                // Generate the collisions with the 'long edges' where we'll be considering casting more rays later on
242                let extra_collisions    = long_edges.iter()
243                    .enumerate()
244                    .filter(|(edge_index, _edge)| *edge_index != long_edge_index)
245                    .filter_map(move |(edge_index, edge)| {
246                        // Create lines from the ray and the lines
247                        let edge_line   = (edge.start, edge.end);
248
249                        // Detect where they intersect
250                        if let Some(intersection_point) = line_intersects_ray(&edge_line, &ray_line) {
251                            // Move the intersection point slightly inside the shape along the direction of the ray (so we can add the final result up properly)
252                            let length              = to.distance_to(&from);
253                            let direction           = (to-from) * (4.0/length);
254                            let intersection_point  = intersection_point + (direction * self_intersection_distance);
255
256                            // Generate a colision at this point
257                            Some(RayCollision {
258                                position:   intersection_point,
259                                what:       ConcaveItem::SelfIntersection(edge_index)
260                            })
261                        } else {
262                            None
263                        }
264                    });
265
266                // Combine the two sets to generate the final set of collisions
267                edge_collisions.into_iter().chain(extra_collisions)
268            };
269
270            // Perform raycasting over a 180 degree angle to get the next set of edges
271            let mut new_edges   = trace_outline_convex_partial(center_point, options, line_angle..(line_angle+f64::consts::PI), cast_ray_to_edges);
272
273            if new_edges.len() > 2 {
274                // We ignore the first point as it will be the point along the existing edge (ie, will be the start point we already know)
275                new_edges.remove(0);
276                let next_edge_index = next_edge.edge_index.1;
277
278                // Invalidate any edge we've had an intersection with (we'll end up with a 0-width gap we'll try to fill if we process these)
279                for new_edge in new_edges.iter() {
280                    if let ConcaveItem::SelfIntersection(edge_index) = new_edge.what {
281                        long_edges[edge_index].ray_collided = true;
282                    }
283                }
284
285                // Find new long edges in the new edges
286                let mut new_long_edges  = find_long_edges(&new_edges[0..(new_edges.len())], edge_min_len_squared);
287
288                // Don't count the edge ending at point 0 (that's the edge we just came from)
289                new_long_edges.retain(|edge| edge.edge_index.1 != 0);
290
291                // Remove any gaps if necessary
292                if let Some(min_gap) = options.min_gap {
293                    remove_small_gaps(&center, &mut new_edges, &mut new_long_edges, min_gap);
294                }
295
296
297                // Insert the new edges into the existing edge list (except the first which will be a duplicate)
298                let edge_index      = next_edge_index;
299                let num_new_edges   = new_edges.len()-1;
300                edges.splice(edge_index..edge_index, new_edges.into_iter().take(num_new_edges));
301
302                // Update the remaining long edge indexes
303                for long_edge in long_edges.iter_mut() {
304                    if long_edge.edge_index.0 >= edge_index {
305                        long_edge.edge_index.0 += num_new_edges;
306                    }
307
308                    if long_edge.edge_index.1 >= edge_index {
309                        long_edge.edge_index.1 += num_new_edges;
310                    }
311                }
312
313                // Add the new long edges to the list
314                for edge in new_long_edges.iter_mut() {
315                    edge.edge_index.0 += edge_index;
316                    edge.edge_index.1 += edge_index;
317                }
318
319                long_edges.splice((long_edge_index+1)..(long_edge_index+1), new_long_edges);
320            }
321        }
322
323        // Check the next edge
324        long_edge_index += 1;
325    }
326
327    // The edges we retrieved are the result
328    edges.into_iter()
329        .map(|collision| RayCollision { 
330            position:   collision.position,
331            what:       collision.what.into()
332        })
333        .collect()
334}
335
336///
337/// Creates a Bezier path by flood-filling a convex area whose bounds can be determined by ray-casting.
338/// 
339/// This won't fill areas that cannot be directly reached by a straight line from the center point. If the
340/// area is not entirely closed (from the point of view of the ray-casting function), then a line will be
341/// generated between the gaps.
342///
343pub fn flood_fill_concave<Path, Coord, Item, RayList, RayFn>(center: Coord, options: &FillSettings, cast_ray: RayFn) -> Option<Vec<Path>>
344where  
345    Path:       BezierPathFactory<Point=Coord>,
346    Coord:      Coordinate+Coordinate2D,
347    RayList:    IntoIterator<Item=RayCollision<Coord, Item>>,
348    RayFn:      Fn(Coord, Coord) -> RayList,
349{
350    // Trace where the ray casting algorithm indicates collisions with the specified center
351    let collisions = trace_outline_concave(center, options, cast_ray);
352
353    // Build a path using the LMS algorithm
354    let curves = fit_curve_loop::<Curve<Coord>>(&collisions.iter().map(|collision| collision.position).collect::<Vec<_>>(), options.fit_error);
355
356    if let Some(curves) = curves {
357        if !curves.is_empty() {
358            // Convert the curves into a path
359            let initial_point   = curves[0].start_point();
360            let overlapped_path  = Path::from_points(initial_point, curves.into_iter().map(|curve| {
361                let (cp1, cp2)  = curve.control_points();
362                let end_point   = curve.end_point();
363                (cp1, cp2, end_point)
364            }));
365            
366            // Remove any interior points that the path might have (this happens when the fill path overlaps itself)
367            Some(path_remove_interior_points(&vec![overlapped_path], 0.01))
368        } else {
369            // No curves in the path
370            None
371        } 
372    } else {
373        // Failed to fit a curve to these points
374        None
375    }
376}