1#![doc(hidden)]
2use crate::*;
3#[cfg(test)]
4use alloc::{vec, vec::Vec};
5#[cfg(test)]
6use approx::assert_abs_diff_eq;
7#[cfg(test)]
8use core::iter::zip;
9pub use util::dist_err as curve_diff;
10
11pub const EPS: f64 = 2.2e-14;
13pub const RES: usize = 1000;
14
15#[test]
16fn error() {
17 let coeff = vec![
18 na::matrix![10., 20.; 20., 10.],
19 na::matrix![ 3., 4.; 4., 3.],
20 ];
21 let a = Efd2::from_coeffs_unchecked(coeff);
22 let coeff = vec![na::matrix![10., 20.; 20., 10.]];
23 let b = Efd2::from_coeffs_unchecked(coeff);
24 assert_eq!(a.square_err(&b), 50.);
25 assert_eq!(b.square_err(&a), 50.);
26}
27
28#[test]
29fn efd2d() {
30 let efd = Efd2::from_curve(CURVE2D, false);
31 assert!(!efd.is_open());
32 let curve = CURVE2D
34 .iter()
35 .cycle()
36 .skip(CURVE2D.len() / 2)
37 .take(CURVE2D.len())
38 .copied()
39 .collect::<Vec<_>>();
40 let efd_half = Efd2::from_curve_harmonic(curve, false, efd.harmonic());
41 assert_abs_diff_eq!(efd.err(&efd_half), 0., epsilon = 1e-12);
42 assert_eq!(efd.harmonic(), 8);
43 for ang in 0..6 {
45 let ang = core::f64::consts::TAU * ang as f64 / 6.;
46 let curve = GeoVar::from_rot(na::UnitComplex::new(ang)).transform(CURVE2D);
47 let efd_rot = Efd2::from_curve_harmonic(curve, false, efd.harmonic());
48 assert_abs_diff_eq!(efd_rot.err(&efd), 0., epsilon = 1e-12);
49 }
50 let geo = efd.as_geo();
52 assert_abs_diff_eq!(geo.trans()[0], -1.248409055632358);
53 assert_abs_diff_eq!(geo.trans()[1], 55.26080122817753);
54 assert_abs_diff_eq!(geo.rot().angle(), -2.49925101855502);
55 assert_abs_diff_eq!(geo.scale(), 48.16765830752243);
56 let target = efd.recon_norm_by(&get_norm_t(CURVE2D, false));
58 let curve = efd.as_geo().inverse().transform(CURVE2D);
59 assert_abs_diff_eq!(curve_diff(target, curve), 0., epsilon = 0.0029);
60}
61
62#[test]
63fn efd2d_open() {
64 let efd = Efd2::from_curve(CURVE2D_OPEN, true);
65 assert!(efd.is_open());
66 assert_eq!(efd.harmonic(), 14);
67 for ang in 0..6 {
69 let ang = core::f64::consts::TAU * ang as f64 / 6.;
70 let curve = GeoVar::from_rot(na::UnitComplex::new(ang)).transform(CURVE2D_OPEN);
71 let efd_rot = Efd2::from_curve_harmonic(curve, true, efd.harmonic());
72 assert_abs_diff_eq!(efd_rot.err(&efd), 0., epsilon = 1e-12);
73 }
74 let geo = efd.as_geo();
76 assert_abs_diff_eq!(geo.trans()[0], 43.03456791427352);
77 assert_abs_diff_eq!(geo.trans()[1], 48.107208358019015);
78 assert_abs_diff_eq!(geo.rot().angle(), 2.7330524299596815);
79 assert_abs_diff_eq!(geo.scale(), 33.930916934329495);
80 let target = efd.recon_norm_by(&get_norm_t(CURVE2D_OPEN, true));
82 let curve = efd.as_geo().inverse().transform(CURVE2D_OPEN);
83 assert_abs_diff_eq!(curve_diff(target, curve), 0., epsilon = 0.0411);
84}
85
86#[test]
87fn efd3d() {
88 let efd = Efd3::from_curve(CURVE3D, false);
89 assert!(!efd.is_open());
90 let curve = CURVE3D
92 .iter()
93 .cycle()
94 .skip(CURVE3D.len() / 2)
95 .take(CURVE3D.len())
96 .copied()
97 .collect::<Vec<_>>();
98 let efd_half = Efd3::from_curve_harmonic(curve, false, efd.harmonic());
99 assert_abs_diff_eq!(efd.err(&efd_half), 0., epsilon = 1e-12);
100 assert_eq!(efd.harmonic(), 5);
101 for ang in 0..6 {
103 let ang = core::f64::consts::TAU * ang as f64 / 6.;
104 let curve = GeoVar::from_rot(na::UnitQuaternion::new(na::matrix![1.; 1.; 0.] * ang))
105 .transform(CURVE3D);
106 let efd_rot = Efd3::from_curve_harmonic(curve, false, efd.harmonic());
107 assert_abs_diff_eq!(efd.err(&efd_rot), 0., epsilon = 1e-12);
108 }
109 let geo = efd.as_geo();
111 assert_abs_diff_eq!(geo.trans()[0], 0.7239345388499508);
112 assert_abs_diff_eq!(geo.trans()[1], 0.09100107896533066);
113 assert_abs_diff_eq!(geo.trans()[2], 0.49979194975846675);
114 assert_abs_diff_eq!(geo.rot().angle(), 2.9160714030359416);
115 assert_abs_diff_eq!(geo.scale(), 0.5629099155595344);
116 let target = efd.recon_norm_by(&get_norm_t(CURVE3D, false));
118 let curve = efd.as_geo().inverse().transform(CURVE3D);
119 assert_abs_diff_eq!(curve_diff(target, curve), 0., epsilon = 0.0013);
120}
121
122#[test]
123fn posed_efd2d_open() {
124 let efd = PosedEfd2::from_angles(CURVE2D_POSE, ANGLE2D_POSE, true);
125 for ang in 0..6 {
127 let ang = core::f64::consts::TAU * ang as f64 / 6.;
128 let curve = GeoVar::from_rot(na::UnitComplex::new(ang)).transform(CURVE2D_POSE);
129 let angles = ANGLE2D_POSE.iter().map(|a| a + ang).collect::<Vec<_>>();
130 let efd_rot = PosedEfd2::from_angles(curve, &angles, true);
131 assert_abs_diff_eq!(efd.err(&efd_rot), 0., epsilon = 1e-12);
132 }
133}
134
135#[test]
136#[cfg(feature = "std")]
137fn plot2d_harmonic() -> Result<(), Box<dyn std::error::Error>> {
138 const N: usize = 8;
139 let mut efd = Efd2::from_curve_harmonic(ORIGIN_CURVE, false, N);
140 *efd.as_geo_mut() *= GeoVar2::from_scale(20.);
141 plot2d(&efd, format!("img/2dh{N}.svg"))?;
142 for n in (1..N).rev() {
143 efd.set_harmonic(n);
144 plot2d(&efd, format!("img/2dh{n}.svg"))?;
145 }
146 Ok(())
147}
148
149#[test]
150#[cfg(feature = "std")]
151fn plot2d_closed() -> Result<(), Box<dyn std::error::Error>> {
152 let efd = Efd::from_coeffs_unchecked(vec![
153 na::matrix![12., 35.; 35., 13.],
154 na::matrix![5., 21.; 21., 5.],
155 na::matrix![1., 12.; 12., 1.],
156 ]);
157 plot2d(&efd, "img/2d.svg")
158}
159
160#[test]
161#[cfg(feature = "std")]
162fn plot2d_open() -> Result<(), Box<dyn std::error::Error>> {
163 let efd = Efd::from_coeffs_unchecked(vec![
164 na::matrix![35., 0.; 8., 0.],
165 na::matrix![10., 0.; 24., 0.],
166 na::matrix![5., 0.; -8., 0.],
167 ]);
168 plot2d(&efd, "img/2d_open.svg")
169}
170
171#[test]
172#[cfg(feature = "std")]
173fn plot3d_closed() -> Result<(), Box<dyn std::error::Error>> {
174 let efd = Efd::from_coeffs_unchecked(vec![
175 na::matrix![12., 22.; 35., 5.; 20., 21.],
176 na::matrix![21., 12.; 5., 12.; 1., 1.],
177 na::matrix![3., 3.; 7., 5.; 12., 21.],
178 ]);
179 plot3d(&efd, "img/3d.svg")
180}
181
182#[test]
183#[cfg(feature = "std")]
184fn plot3d_open() -> Result<(), Box<dyn std::error::Error>> {
185 let efd = Efd::from_coeffs_unchecked(vec![
186 na::matrix![16., 0.; 35., 0.; 27., 0.],
187 na::matrix![21., 0.; 8., 0.; 16., 0.],
188 na::matrix![3., 0.; 7., 0.; 12., 0.],
189 ]);
190 plot3d(&efd, "img/3d_open.svg")
191}
192
193#[cfg(all(test, feature = "std"))]
194fn get_area<const D: usize>(pts: &[[f64; D]]) -> [[f64; 2]; D] {
195 pts.iter()
196 .fold([[f64::INFINITY, f64::NEG_INFINITY]; D], |mut b, c| {
197 zip(&mut b, c).for_each(|(b, c)| *b = [b[0].min(*c), b[1].max(*c)]);
198 b
199 })
200}
201
202#[cfg(all(test, feature = "std"))]
203fn plot2d(efd: &Efd2, path: impl AsRef<std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
204 use plotters::prelude::*;
205
206 fn bounding_box(pts: &[[f64; 2]]) -> [f64; 4] {
207 let [[x_min, x_max], [y_min, y_max]] = get_area(pts);
208 let dx = (x_max - x_min).abs();
209 let dy = (y_max - y_min).abs();
210 if dx > dy {
211 let cen = (y_min + y_max) * 0.5;
212 let r = dx * 0.5;
213 let mg = dx * 0.1;
214 [x_min - mg, x_max + mg, cen - r - mg, cen + r + mg]
215 } else {
216 let cen = (x_min + x_max) * 0.5;
217 let r = dy * 0.5;
218 let mg = dy * 0.1;
219 [cen - r - mg, cen + r + mg, y_min - mg, y_max + mg]
220 }
221 }
222
223 let curve = efd.recon(360);
224 let [x_min, x_max, y_min, y_max] = bounding_box(&curve);
225 let b = SVGBackend::new(&path, (1200, 1200));
226 let root = b.into_drawing_area();
227 root.fill(&WHITE)?;
228 let mut chart = ChartBuilder::on(&root).build_cartesian_2d(x_min..x_max, y_min..y_max)?;
229 let p0 = curve[0];
230 chart.draw_series([Circle::new((p0[0], p0[1]), 3, BLACK.filled())])?;
231 for (p, color) in [((10., 0.), RED), ((0., 10.), BLUE)] {
232 chart.draw_series(LineSeries::new([(0., 0.), p], color.stroke_width(10)))?;
233 }
234 let geo0 = efd.as_geo();
235 let mut c0 = [0.; 2];
236 for m in efd.coeffs_iter() {
237 let geo = geo0 * GeoVar2::new(c0, na::UnitComplex::new(0.), 1.);
238 const N: usize = 100;
239 const N_F: f64 = N as f64;
240 let ellipse = (0..N)
241 .map(|i| {
242 let t = i as f64 * std::f64::consts::TAU / N_F;
243 m * na::matrix![t.cos(); t.sin()]
244 })
245 .map(|c| geo.transform_pt(c.data.0[0]).into());
246 let p1 = c0;
247 zip(&mut c0, &m.column(0)).for_each(|(c, u)| *c += u);
248 let p1 = geo0.transform_pt(p1).into();
249 let p2 = geo0.transform_pt(c0).into();
250 chart.draw_series([Circle::new(p2, 5, RED.filled())])?;
251 chart.draw_series(LineSeries::new([p1, p2], RED.stroke_width(5)))?;
252 chart.draw_series(LineSeries::new(ellipse, RED.stroke_width(7)))?;
253 }
254 chart.draw_series(LineSeries::new(
255 curve.into_iter().map(|c| c.into()),
256 BLACK.stroke_width(10),
257 ))?;
258 Ok(())
259}
260
261#[cfg(all(test, feature = "std"))]
262fn plot3d(efd: &Efd3, path: impl AsRef<std::path::Path>) -> Result<(), Box<dyn std::error::Error>> {
263 use plotters::prelude::*;
264
265 fn bounding_box(pts: &[[f64; 3]]) -> [f64; 6] {
266 let mut b = get_area(pts);
267 let center = b.map(|[min, max]| (min + max) * 0.5);
268 let width = b
269 .iter()
270 .map(|[min, max]| (max - min).abs())
271 .fold(0., f64::max);
272 for ([min, max], c) in zip(&mut b, center) {
273 *min = c - width * 0.5;
274 *max = c + width * 0.5;
275 }
276 let [[x_min, x_max], [y_min, y_max], [z_min, z_max]] = b;
277 [x_min, x_max, y_min, y_max, z_min, z_max]
278 }
279
280 let curve = efd.recon(360);
281 let [x_min, x_max, y_min, y_max, z_min, z_max] = bounding_box(&curve);
282 let b = SVGBackend::new(&path, (1200, 1200));
283 let root = b.into_drawing_area();
284 root.fill(&WHITE)?;
285 let mut chart =
286 ChartBuilder::on(&root).build_cartesian_3d(x_min..x_max, y_min..y_max, z_min..z_max)?;
287 chart.with_projection(|mut pb| {
288 pb.yaw = 0.3;
289 pb.scale = 1.4;
290 pb.into_matrix()
291 });
292 let p0 = curve[0];
293 chart.draw_series([Circle::new((p0[0], p0[1], p0[2]), 3, BLACK.filled())])?;
294 for (p, color) in [
295 ((10., 0., 0.), RED),
296 ((0., 10., 0.), BLUE),
297 ((0., 0., 10.), GREEN),
298 ] {
299 chart.draw_series(LineSeries::new([(0., 0., 0.), p], color.stroke_width(10)))?;
300 }
301 let geo0 = efd.as_geo();
302 let mut c0 = [0.; 3];
303 for m in efd.coeffs_iter() {
304 let geo = geo0 * GeoVar3::new(c0, na::UnitQuaternion::identity(), 1.);
305 const N: usize = 100;
306 const N_F: f64 = N as f64;
307 let ellipse = (0..N)
308 .map(|i| {
309 let t = i as f64 * std::f64::consts::TAU / N_F;
310 m * na::matrix![t.cos(); t.sin()]
311 })
312 .map(|c| geo.transform_pt(c.data.0[0]).into());
313 let p1 = c0;
314 zip(&mut c0, &m.column(0)).for_each(|(c, u)| *c += u);
315 let p1 = geo0.transform_pt(p1).into();
316 let p2 = geo0.transform_pt(c0).into();
317 chart.draw_series([Circle::new(p2, 5, RED.filled())])?;
318 chart.draw_series(LineSeries::new([p1, p2], RED.stroke_width(5)))?;
319 chart.draw_series(LineSeries::new(ellipse, RED.stroke_width(7)))?;
320 }
321 chart.draw_series(LineSeries::new(
322 curve.into_iter().map(|c| c.into()),
323 BLACK.stroke_width(10),
324 ))?;
325 Ok(())
326}
327
328pub const CURVE2D: &[[f64; 2]] = &[
329 [14.928108089437242, 90.01002059789568],
330 [-3.25371009238094, 85.46456605244113],
331 [-16.763462931659024, 76.52439024390245],
332 [-39.6173464560173, 57.055475143350215],
333 [-49.46583130450215, 35.085778173653246],
334 [-27.739072687756586, 14.939024390243903],
335 [-2.117346456017304, 19.17668726456234],
336 [17.958411119740273, 37.7372933251684],
337 [26.291744453073605, 57.81305090092597],
338 [43.71598687731603, 68.41911150698658],
339 [47.12507778640693, 80.5403236281987],
340 [38.41295657428572, 90.38880847668355],
341 [27.80689596822512, 91.1463842342593],
342];
343pub const CURVE2D_OPEN: &[[f64; 2]] = &[
344 [0.028607755880487345, 47.07692307692308],
345 [6.182453909726641, 52.76923076923077],
346 [14.797838525111256, 57.07692307692308],
347 [24.643992371265103, 58.61538461538461],
348 [41.10553083280357, 59.07692307692308],
349 [50.18245390972664, 56.76923076923077],
350 [60.6439923712651, 51.53846153846154],
351 [65.41322314049587, 46.0],
352 [68.79783852511126, 36.92307692307692],
353 [67.41322314049587, 25.384615384615383],
354 [60.6439923712651, 18.153846153846153],
355];
356#[rustfmt::skip]
357pub const CURVE3D: &[[f64; 3]] = &[
358 [0.6999100262927096,0.43028119112952473,0.5700737247541725],
359 [0.6296590344074013,0.4512760872295425,0.6323601770225047],
360 [0.5638138739696974,0.46183051089148464,0.6847090584540213],
361 [0.5058369206546075,0.4615280764519245,0.7287803814245077],
362 [0.45702863707701546,0.4510622388540364,0.7665948614304103],
363 [0.41723197726966244,0.4317766784472379,0.799678921250722],
364 [0.3856541747672426,0.40523683682222406,0.8288871838597307],
365 [0.3614034251971859,0.3729768425814701,0.8545617819407205],
366 [0.3437381466626122,0.33639334903869056,0.8767460300745514],
367 [0.3321447389761846,0.29671906154191235,0.8953422088163436],
368 [0.3263372629749841,0.25502826620566654,0.9102002934684913],
369 [0.32622858809088523,0.21225076122841705,0.9211539082423659],
370 [0.3318929803861661,0.16918542545203516,0.928021196624841],
371 [0.34352480290022414,0.1265118028240236,0.9305834049340109],
372 [0.36139084538662236,0.08480105307198857,0.9285501808026447],
373 [0.38577056486989364,0.04452862638492877,0.9215195454857344],
374 [0.41687739559371884,0.0060909757379047635,0.9089423177834546],
375 [0.45475516434546037,-0.030172412132808968,0.8901052556002697],
376 [0.49914754370187137,-0.06395289690096129,0.8641537806399759],
377 [0.5493470560271839,-0.09494849409160033,0.8301822664355119],
378 [0.6040449685807827,-0.1228552958162816,0.7874238072487618],
379 [0.6612249903953387,-0.14739768525858138,0.7355647044666407],
380 [0.7181712990997067,-0.1684251622294386,0.6751762361616609],
381 [0.7716940290779204,-0.18608109658863914,0.6081629312757029],
382 [0.8186957920221034,-0.20095221972437277,0.5379176568148215],
383 [0.8570911228971989,-0.21393997152327254,0.46864111603170266],
384 [0.88661962597743,-0.2256744734813517,0.403703691896119],
385 [0.9087058112647335,-0.2359493070484034,0.3443568978212587],
386 [0.9253987983439792,-0.24384980367577586,0.29012813939844717],
387 [0.9384640530551117,-0.24830445184260166,0.24006274246229356],
388 [0.9491216566898215,-0.2484529388271337,0.19349216519158882],
389 [0.9581400770939057,-0.2437369857356437,0.1501994489037393],
390 [0.9659871776934699,-0.2338686278496317,0.11033692690815355],
391 [0.9729414021915203,-0.21877677851516128,0.07430847249221559],
392 [0.979158591144934,-0.1985626427303512,0.04267704651223003],
393 [0.984708076435491,-0.17346680001041803,0.0161019718314504],
394 [0.9895891445905367,-0.1438444290548901,-0.004701610196814504],
395 [0.9937341248474196,-0.11014503640716575,-0.01898841932855333],
396 [0.997000753888979,-0.07289400668205587,-0.02599924103976281],
397 [0.9991540450179144,-0.03267407497300495,-0.024971967263275538],
398 [0.9998362404365794,0.009894685600921771,-0.015152145277328999],
399 [0.9985225360959517,0.05417737861443478,0.004190054292864898],
400 [0.9944609178696164,0.09954962956012486,0.03372468064136638],
401 [0.9865989401537969,0.14542428454053347,0.07398857177482451],
402 [0.9735136458469561,0.19127348741773503,0.12524230260109778],
403 [0.9533914378420906,0.2366305939786505,0.18721839718014333],
404 [0.9241589851285424,0.2810406966408692,0.25873982499375203],
405 [0.8839235581077262,0.32391579444519214,0.33727985639690555],
406 [0.8318395864591331,0.36427712952733915,0.4187422540212782],
407 [0.7691918202384619,0.4004903064827955,0.49794724428553494],
408];
409pub const CURVE2D_POSE: &[[f64; 2]] = &[
410 [18.8, 12.1],
411 [13.3, 18.1],
412 [6.3, 19.8],
413 [-0.4, 17.1],
414 [-2.7, 10.3],
415 [-1.1, 6.0],
416 [0.2, 1.7],
417 [3.4, -2.2],
418 [7.8, -4.9],
419];
420pub const ANGLE2D_POSE: &[f64] = &[-0.9, 0., 0.7, 1.5, 2.8, -2.3, -2., -1.9, -2.1];
421pub const ORIGIN_CURVE: &[[f64; 2]] = &[
422 [0., 0.],
423 [1., 1.],
424 [1., 2.],
425 [0., 3.],
426 [1., 3.],
427 [2., 3.],
428 [3., 3.],
429 [2., 2.],
430 [2., 1.],
431 [3., 0.],
432 [3., -1.],
433 [3., -2.],
434 [2., -2.],
435 [1., -2.],
436 [0., -2.],
437 [0., -1.],
438 [0., 0.],
439];