aoer_plotty_rs/optimizer/
mod.rs1use std::collections::HashMap;
2
3use geo::{prelude::EuclideanDistance, HasDimensions};
4use geo_types::{Coord as Coordinate, LineString, MultiLineString};
5use rstar::{PointDistance, RTree, RTreeObject, AABB};
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum OptimizationStrategy {
9 Greedy,
10}
11
12#[derive(Debug, Clone, PartialEq)]
13pub struct Optimizer {
15 max_keepdown: f64,
16 strategy: OptimizationStrategy,
17}
18
19impl Optimizer {
20 pub fn new(max_keepdown: f64, strategy: OptimizationStrategy) -> Optimizer {
21 Optimizer {
22 max_keepdown,
23 strategy,
24 }
25 }
26
27 pub fn build_rtree_from_hashmap(
28 &self,
29 hashmap: &HashMap<usize, LineString<f64>>,
30 ) -> RTree<LineRef> {
31 let mut linerefs: Vec<LineRef> = vec![];
33 for (i, line) in hashmap.iter() {
34 if line.0.len() < 2 {
36 continue;
37 }
38 linerefs.push(LineRef::new(
39 *i,
40 line.0
41 .first()
42 .expect("We already bounds checked this?!")
43 .clone(),
44 line.0
45 .last()
46 .expect("We already bounds checked this too!?")
47 .clone(),
48 self.max_keepdown * 2., true,
50 ));
51 linerefs.push(LineRef::new(
52 *i,
53 line.0
54 .last()
55 .expect("We already bounds checked this?!")
56 .clone(),
57 line.0
58 .first()
59 .expect("We already bounds checked this too!?")
60 .clone(),
61 self.max_keepdown * 2., false,
63 ));
64 }
65
66 RTree::bulk_load(linerefs)
67 }
68
69 pub fn merge(&self, mls: &MultiLineString<f64>) -> MultiLineString<f64> {
71 let mut lines_out = MultiLineString::new(vec![]); let mut current_line: LineString<f64> = LineString::new(vec![]);
73 for source_line in mls.0.iter() {
74 if source_line.0.len() == 0 {
75 continue;
76 } let source_start = source_line.0.first().unwrap();
78 if current_line.0.len() == 0 {
79 current_line.0.append(&mut source_line.0.clone());
80 } else if current_line
81 .0
82 .last()
83 .unwrap()
84 .euclidean_distance(source_start)
85 <= self.max_keepdown
86 {
87 let mut tmpline = source_line.0.clone();
88 current_line.0.append(&mut tmpline);
89 } else {
90 lines_out.0.push(current_line.clone());
91 current_line = source_line.clone();
92 }
93 }
94 if current_line.0.len() > 0 {
95 lines_out.0.push(current_line)
96 }
97 lines_out
98 }
99
100 pub fn optimize(&self, mls: &MultiLineString<f64>) -> MultiLineString<f64> {
103 assert_eq!(self.strategy, OptimizationStrategy::Greedy);
104 let mut lines_out = MultiLineString::new(vec![]);
105 if mls.0.len() == 0 {
107 return lines_out;
108 };
109 if mls.0.len() == 1 {
110 return mls.clone();
111 }
112 let mut line_count: usize = 0;
113 let mut lines_hash: HashMap<usize, LineString<f64>> =
114 HashMap::from_iter(mls.0.iter().map(|line| {
115 let pair = (line_count, line.clone());
116 line_count = line_count + 1;
117 pair
118 }));
119 let rtree = self.build_rtree_from_hashmap(&lines_hash);
120
121 while lines_hash.len() > 0 {
122 if let Some(tmpline) = lines_hash.remove(&0) {
123 if tmpline.is_empty() {
124 continue;
125 }
126 if tmpline.0.len() > 1 {
127 lines_out.0.push(tmpline); break;
129 } else {
130 }
131 } else {
132 break;
133 }
134 }
135 while lines_hash.len() > 0 {
136 let line = lines_out
137 .0
138 .last()
139 .expect("Cannot pull line from list I just pushed to?!");
140 let last = match line.0.last() {
141 Some(point) => point,
142 None => {
143 eprintln!("Failed to get last point for line: {:?}", &line);
144 continue;
145 }
146 };
147 let mut found = false;
148 for neighbor_ref in rtree.nearest_neighbor_iter(&[last.x, last.y]) {
149 if let Some(mut neighbor_line) = lines_hash.remove(&neighbor_ref.line_id) {
150 found = true;
151 if neighbor_ref.fwd {
152 lines_out.0.push(neighbor_line);
153 } else {
154 neighbor_line.0.reverse();
155 lines_out.0.push(neighbor_line);
156 }
157 break;
158 }
159 }
160 if !found {
161 break;
162 }
163 } let remaining_keys: Vec<usize> = lines_hash.keys().map(|k| k.clone()).collect();
165 for k in remaining_keys {
166 if let Some(line) = lines_hash.remove(&k) {
167 lines_out.0.push(line);
168 }
169 }
170
171 lines_out
172 }
173}
174
175#[derive(Clone, Debug, PartialEq)]
179pub struct LineRef {
180 line_id: usize,
181 start: Coordinate<f64>,
182 end: Coordinate<f64>,
183 pen_width: f64,
184 fwd: bool,
185}
186
187impl LineRef {
188 pub fn new(
189 line_id: usize,
190 start: Coordinate<f64>,
191 end: Coordinate<f64>,
192 pen_width: f64,
193 fwd: bool,
194 ) -> LineRef {
195 LineRef {
196 line_id,
197 start,
198 end,
199 pen_width,
200 fwd,
201 }
202 }
203}
204
205impl RTreeObject for LineRef {
206 type Envelope = AABB<[f64; 2]>;
207
208 fn envelope(&self) -> Self::Envelope {
209 AABB::from_corners(
210 [self.start.x - self.pen_width, self.start.y - self.pen_width],
211 [self.start.x + self.pen_width, self.start.y + self.pen_width],
212 )
213 }
214}
215
216impl PointDistance for LineRef {
217 fn distance_2(&self, point: &[f64; 2]) -> f64 {
218 let d_x = self.start.x - point[0];
219 let d_y = self.start.y - point[1];
220 let distance_to_start = (d_x * d_x + d_y * d_y).sqrt();
221 let distance_to_ring = distance_to_start - self.pen_width / 2.;
222 let distance_to_circle = f64::max(0.0, distance_to_ring);
223 distance_to_circle * distance_to_circle
225 }
226
227 fn contains_point(&self, point: &[f64; 2]) -> bool {
230 let d_x = self.start.x - point[0];
231 let d_y = self.start.y - point[1];
232 let distance_to_start_2 = d_x * d_x + d_y * d_y;
233 let radius_2 = self.pen_width / 2. * self.pen_width / 2.;
234 distance_to_start_2 <= radius_2
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use std::sync::Arc;
241
242 use crate::prelude::LineHatch;
243
244 use super::*;
245 use geo_types::{coord, Polygon};
246 use rstar::RTree;
247 use wkt::ToWkt;
248
249 #[test]
250 fn test_optimizer_poly_nest() {
251 let mut ctx = crate::context::Context::new();
252 ctx.pen(1.)
253 .stroke("#000")
254 .fill("#000")
255 .hatch(0.)
256 .pen(0.5)
257 .pattern(Arc::new(Box::new(LineHatch {})));
258 let outer = LineString::new(vec![
259 coord! {x:0., y:10.},
260 coord! {x:14., y: 0.},
261 coord! {x:-14., y:0.},
262 coord! {x:0., y:10.},
263 ]);
264 let inner = vec![LineString::new(vec![
265 coord! {x:0., y:6.},
266 coord! {x:4.,y:4.},
267 coord! {x:-4., y:4.},
268 coord! {x:0., y:6.},
269 ])];
270 let test_poly = Polygon::<f64>::new(outer, inner);
271 ctx.geometry(&geo_types::Geometry::Polygon(test_poly));
272 for layer in ctx.to_layers() {
273 println!("LAYER:");
274 println!("STROKE: {}", layer.stroke_lines.to_wkt());
275 println!("FILL: {}", layer.fill_lines.to_wkt());
276 }
277 }
278
279 #[test]
280 fn test_optimizer_infill_premerged() {
281 let mut ctx = crate::context::Context::new();
282 ctx.pen(1.)
283 .stroke("#000")
284 .fill("#000")
285 .hatch(0.)
286 .pattern(Arc::new(Box::new(LineHatch {})))
287 .circle(0., 0., 10.);
288 let layers = ctx.to_layers();
289 for layer in layers {
290 let opt = Optimizer::new(layer.stroke_width * 1.5, OptimizationStrategy::Greedy);
291 let opt_stroke = opt.optimize(&layer.stroke_lines);
292 let merged_fill = opt.merge(&layer.fill_lines);
293 let opt_fill = opt.optimize(&merged_fill);
294 println!("LAYER:");
295 println!("STROKE: {}", opt_stroke.to_wkt());
296 println!("FILL: {}", opt_fill.to_wkt());
297 }
298 }
299
300 #[test]
301 fn test_optimizer_infill() {
302 let mut ctx = crate::context::Context::new();
303 ctx.pen(1.)
304 .stroke("#000")
305 .fill("#000")
306 .hatch(0.)
307 .pattern(Arc::new(Box::new(LineHatch {})))
308 .circle(0., 0., 10.);
309 let layers = ctx.to_layers();
310 for layer in layers {
311 let opt = Optimizer::new(layer.stroke_width, OptimizationStrategy::Greedy);
312 let opt_stroke = opt.optimize(&layer.stroke_lines);
313 let opt_fill = opt.optimize(&layer.fill_lines);
314 println!("LAYER:");
315 println!("STROKE: {}", opt_stroke.to_wkt());
316 println!("FILL: {}", opt_fill.to_wkt());
317 }
318 }
319
320 #[test]
321 fn test_lineref_basic() {
322 let line_0f = LineRef {
323 line_id: 0,
324 start: coord! {x:0., y:0.},
325 end: coord! {x:20., y:0.},
326
327 pen_width: 0.5,
328 fwd: true,
329 };
330 let line_0r = LineRef {
331 line_id: 1,
332 start: coord! {x:20., y:0.},
333 end: coord! {x:0., y:0.},
334 pen_width: 0.5,
335 fwd: false,
336 };
337 let line_1f = LineRef {
338 line_id: 2,
339 start: coord! {x:10., y:10.},
340 end: coord! {x:20., y:5.},
341 pen_width: 1.0,
342 fwd: true,
343 };
344 let line_1r = LineRef {
345 line_id: 3,
346 start: coord! {x:20., y:5.},
347 end: coord! {x:10., y:10.},
348 pen_width: 1.0,
349 fwd: false,
350 };
351 let mut tree = RTree::new();
352 tree.insert(line_0f.clone());
353 tree.insert(line_0r.clone());
354 tree.insert(line_1f.clone());
355 tree.insert(line_1r.clone());
356 let e = AABB::from_point([10., 10.]);
357 println!("e: {:?}", e);
358 println!(
359 "Closest neighbor to e is {:?}",
360 tree.nearest_neighbor(&[10., 10.])
361 );
362
363 for line in tree.nearest_neighbor_iter(&[10., 10.]) {
364 println!("Found line endpoint for line id: {}", line.line_id);
365 }
366 }
367
368 #[test]
369 fn test_optimizer_basic() {
370 let lines: MultiLineString<f64> = MultiLineString::new(vec![
371 LineString::new(vec![coord! {x: 0.0, y:20.0}, coord! {x:0.0, y:0.0}]),
372 LineString::new(vec![coord! {x: 0.0, y:0.0}, coord! {x:20.0, y:20.0}]),
373 LineString::new(vec![coord! {x: 20.0, y:20.5}, coord! {x:40.0, y:20.0}]),
374 LineString::new(vec![coord! {x: 20.0, y:0.5}, coord! {x:20.0, y:20.0}]),
375 LineString::new(vec![coord! {x:40.0, y:20.0}, coord! {x:40.5,y:40.5}]),
376 LineString::new(vec![coord! {x:0.0, y:0.0}, coord! {x:40.5,y:20.5}]),
377 ]);
378 let mut distance_unopt = 0.;
379 for i in 0..lines.0.len() - 1 {
380 let pt0 = lines.0[i].0.last().unwrap();
381 let pt1 = lines.0[i + 1].0.first().unwrap();
382 let ls0 = pt0.euclidean_distance(pt1);
383 distance_unopt = distance_unopt + ls0;
384 }
385 println!("UNOPT TRAVEL: {}", distance_unopt);
386 let opt = Optimizer::new(0.7, OptimizationStrategy::Greedy);
387 let out = opt.optimize(&lines);
388 println!("OUT OPT: ");
389 for pt in &out.0 {
390 println!("PT: {:?}", pt);
391 }
392 let mut distance_opt = 0.;
393 for i in 0..out.0.len() - 1 {
394 let pt0 = out.0[i].0.last().unwrap();
395 let pt1 = out.0[i + 1].0.first().unwrap();
396 let ls0 = pt0.euclidean_distance(pt1);
397 distance_opt = distance_opt + ls0;
398 }
399 println!("OPT TRAVEL: {}", distance_opt);
400 }
401}