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(¢er, &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(¢er, &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}