1pub mod bfgs;
10pub mod dual;
11pub mod geometry;
12pub mod profile;
13pub mod sketch;
14pub mod solve;
15
16pub use profile::{ProfileEdge, ProfileJoint, ProfileLoop, ProfilePiece, Region};
17pub use sketch::{
18 Constraint, ConstraintId, Curve, CurveId, CurveKind, Point, PointId, Positions, Sketch,
19};
20pub use solve::SolveReport;
21
22#[cfg(test)]
23mod tests {
24 use super::*;
25 use geop_core_math::{
26 for_all_scalars,
27 scalars::{Scalar, scal_in_f64::ScalInF64},
28 vector::Vector2,
29 };
30 use std::f64::consts::{FRAC_PI_2, PI};
31
32 fn close(a: f64, b: f64) -> bool {
33 (a - b).abs() < 1e-7
34 }
35
36 fn xy(s: &Sketch, p: PointId) -> [f64; 2] {
37 s.points[&p].xy()
38 }
39
40 #[test]
43 fn rectangle_is_solved_and_fully_constrained() {
44 let mut s = Sketch::new();
45 let p = [
46 s.add_point(0.1, -0.1),
47 s.add_point(2.2, 0.2),
48 s.add_point(1.9, 1.3),
49 s.add_point(-0.2, 0.8),
50 ];
51 let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
52 s.constrain(Constraint::Fix {
53 point: p[0],
54 x: 0.0,
55 y: 0.0,
56 });
57 s.constrain(Constraint::Horizontal { line: l[0] });
58 s.constrain(Constraint::Horizontal { line: l[2] });
59 s.constrain(Constraint::Vertical { line: l[1] });
60 s.constrain(Constraint::Vertical { line: l[3] });
61 s.constrain(Constraint::Length {
62 curve: l[0],
63 value: 2.0,
64 });
65 s.constrain(Constraint::Distance {
66 a: p[1],
67 b: p[2],
68 value: 1.0,
69 });
70
71 let report = s.solve().unwrap();
72 assert!(report.converged, "{report:?}");
73 assert_eq!(report.dof, 0, "{report:?}");
74 assert!(report.free_points.values().all(|f| !f), "{report:?}");
75 let expect = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]];
76 for (pi, e) in p.iter().zip(expect) {
77 let q = xy(&s, *pi);
78 assert!(close(q[0], e[0]) && close(q[1], e[1]), "{q:?} vs {e:?}");
79 }
80 }
81
82 #[test]
85 fn unanchored_rectangle_has_two_dof() {
86 let mut s = Sketch::new();
87 let p = [
88 s.add_point(0.0, 0.0),
89 s.add_point(2.0, 0.1),
90 s.add_point(2.0, 1.0),
91 s.add_point(0.0, 1.0),
92 ];
93 let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
94 s.constrain(Constraint::Horizontal { line: l[0] });
95 s.constrain(Constraint::Horizontal { line: l[2] });
96 s.constrain(Constraint::Vertical { line: l[1] });
97 s.constrain(Constraint::Vertical { line: l[3] });
98 let report = s.solve().unwrap();
99 assert!(report.converged);
100 assert_eq!(report.dof, 4, "width, height and translation");
101 assert!(report.free_points.values().all(|f| *f));
102 }
103
104 #[test]
107 fn coincident_points_merge() {
108 let mut s = Sketch::new();
109 let a = s.add_point(0.0, 0.0);
110 let b = s.add_point(1.0, 0.0);
111 let c = s.add_point(1.1, 0.05);
112 let d = s.add_point(1.5, 1.0);
113 s.add_line(a, b);
114 s.add_line(c, d);
115 s.constrain(Constraint::Coincident { a: b, b: c });
116 let report = s.solve().unwrap();
117 assert!(report.converged);
118 assert_eq!(xy(&s, b), xy(&s, c));
119 assert_eq!(report.dof, 6);
120 }
121
122 #[test]
125 fn line_arc_tangent_at_shared_endpoint() {
126 let mut s = Sketch::new();
127 let a = s.add_point(0.0, 0.0);
128 let b = s.add_point(2.0, 0.0);
129 let c = s.add_point(2.0, 1.0);
130 let line = s.add_line(a, b);
131 let arc = s.add_arc(b, c, 1.5);
132 s.constrain(Constraint::Fix {
133 point: a,
134 x: 0.0,
135 y: 0.0,
136 });
137 s.constrain(Constraint::Horizontal { line });
138 s.constrain(Constraint::Length {
139 curve: line,
140 value: 2.0,
141 });
142 s.constrain(Constraint::Tangent { a: line, b: arc });
143 s.constrain(Constraint::Radius {
144 curve: arc,
145 value: 0.5,
146 });
147 s.constrain(Constraint::Fix {
148 point: c,
149 x: 2.0,
150 y: 1.0,
151 });
152 let report = s.solve().unwrap();
153 assert!(report.converged, "{report:?}");
154 let CurveKind::Arc { sweep, .. } = s.curves[&arc].kind else {
155 unreachable!()
156 };
157 assert!(close(sweep, PI), "sweep {sweep}");
159 assert_eq!(report.dof, 0, "{report:?}");
160 }
161
162 #[test]
165 fn circle_tangent_to_lines() {
166 let mut s = Sketch::new();
167 let o = s.add_point(0.0, 0.0);
168 let x = s.add_point(3.0, 0.0);
169 let y = s.add_point(0.0, 3.0);
170 let lx = s.add_line(o, x);
171 let ly = s.add_line(o, y);
172 let c = s.add_point(0.8, 1.3);
173 let circle = s.add_circle(c, 0.7);
174 for (p, xy) in [(o, [0.0, 0.0]), (x, [3.0, 0.0]), (y, [0.0, 3.0])] {
175 s.constrain(Constraint::Fix {
176 point: p,
177 x: xy[0],
178 y: xy[1],
179 });
180 }
181 s.constrain(Constraint::Tangent { a: lx, b: circle });
182 s.constrain(Constraint::Tangent { a: circle, b: ly });
183 s.constrain(Constraint::Radius {
184 curve: circle,
185 value: 1.0,
186 });
187 let report = s.solve().unwrap();
188 assert!(report.converged, "{report:?}");
189 let q = xy(&s, c);
190 assert!(close(q[0], 1.0) && close(q[1], 1.0), "{q:?}");
191 }
192
193 #[test]
195 fn conflicting_constraints_do_not_converge() {
196 let mut s = Sketch::new();
197 let a = s.add_point(0.0, 0.0);
198 let b = s.add_point(1.0, 0.0);
199 let l = s.add_line(a, b);
200 s.constrain(Constraint::Length {
201 curve: l,
202 value: 1.0,
203 });
204 s.constrain(Constraint::Distance { a, b, value: 2.0 });
205 let report = s.solve().unwrap();
206 assert!(!report.converged);
207 assert!(!report.failed_constraints.is_empty());
208 }
209
210 #[test]
213 fn drag_follows_cursor_only_where_free() {
214 let mut s = Sketch::new();
215 let a = s.add_point(0.0, 0.0);
216 let b = s.add_point(1.0, 0.0);
217 let l = s.add_line(a, b);
218 s.constrain(Constraint::Fix {
219 point: a,
220 x: 0.0,
221 y: 0.0,
222 });
223 s.constrain(Constraint::Length {
224 curve: l,
225 value: 1.0,
226 });
227 let report = s.solve_with_drag(&[(b, [0.0, 3.0])]).unwrap();
228 assert!(report.converged, "{report:?}");
229 let q = xy(&s, b);
230 assert!(close(q[0], 0.0) && close(q[1], 1.0), "{q:?}");
231
232 let report = s.solve_with_drag(&[(a, [5.0, 5.0])]).unwrap();
233 assert!(report.converged);
234 let q = xy(&s, a);
235 assert!(close(q[0], 0.0) && close(q[1], 0.0), "{q:?}");
236 }
237
238 #[test]
241 fn equilateral_triangle_from_symmetry_and_angle() {
242 let mut s = Sketch::new();
243 let a = s.add_point(-1.0, 0.1);
244 let b = s.add_point(1.2, -0.1);
245 let c = s.add_point(0.1, 1.5);
246 let base = s.add_line(a, b);
247 let left = s.add_line(c, a);
248 let right = s.add_line(c, b);
249 let m = s.add_point(0.0, 0.0);
250 let axis_top = s.add_point(0.0, 2.0);
251 let axis = s.add_line(m, axis_top);
252 s.set_construction(axis, true);
253 s.constrain(Constraint::Fix {
254 point: m,
255 x: 0.0,
256 y: 0.0,
257 });
258 s.constrain(Constraint::Vertical { line: axis });
259 s.constrain(Constraint::Midpoint {
260 point: m,
261 curve: base,
262 });
263 s.constrain(Constraint::Symmetric { a, b, line: axis });
264 s.constrain(Constraint::PointOnCurve {
265 point: c,
266 curve: axis,
267 });
268 s.constrain(Constraint::Equal { a: left, b: right });
269 s.constrain(Constraint::Angle {
270 a: left,
271 b: right,
272 value: PI / 3.0,
273 });
274 s.constrain(Constraint::Length {
275 curve: base,
276 value: 2.0,
277 });
278 let report = s.solve().unwrap();
279 assert!(report.converged, "{report:?}");
280 let q = xy(&s, c);
281 assert!(close(q[0], 0.0) && close(q[1], 3f64.sqrt()), "{q:?}");
282 }
283
284 fn check_slot_with_hole_regions<S: Scalar>() {
288 let mut s = Sketch::new();
289 let p = [
290 s.add_point(0.0, 0.0),
291 s.add_point(2.0, 0.0),
292 s.add_point(2.0, 1.0),
293 s.add_point(0.0, 1.0),
294 ];
295 s.add_line(p[0], p[1]);
296 s.add_arc_with_sweep(p[1], p[2], PI);
297 s.add_line(p[2], p[3]);
298 s.add_arc_with_sweep(p[3], p[0], PI);
299 let c = s.add_point(1.0, 0.5);
300 s.add_circle(c, 0.25);
301 let q = s.add_point(-1.0, -1.0);
303 s.add_line(p[0], q);
304
305 let regions = s.regions().unwrap();
306 assert_eq!(regions.len(), 1);
307 assert_eq!(regions[0].outer.edges.len(), 4);
308 assert_eq!(regions[0].holes.len(), 1);
309
310 let positions = s.positions();
311 for (lp, count) in [(®ions[0].outer, 6), (®ions[0].holes[0], 4)] {
312 let pieces = lp.to_nurbs::<S>(&s, &positions).unwrap();
313 let curves: Vec<_> = pieces.iter().map(|p| &p.curve).collect();
314 assert_eq!(curves.len(), count);
315 for (i, piece) in pieces.iter().enumerate() {
317 assert_eq!(piece.end, pieces[(i + 1) % pieces.len()].start);
318 }
319 for (i, c) in curves.iter().enumerate() {
320 let (t0, t1) = c.domain();
321 assert!(t0.could_be_equal(S::ZERO) && t1.could_be_equal(S::ONE));
322 let next = curves[(i + 1) % curves.len()];
323 let end = c.evaluate(S::ONE).unwrap();
324 let start = next.evaluate(S::ZERO).unwrap();
325 assert!(end.could_be_equal(&start), "{end:?} vs {start:?}");
326 }
327 }
328 let outer = regions[0].outer.to_nurbs::<S>(&s, &positions).unwrap();
330 let far = Vector2::from_array([S::from_f64(2.5), S::from_f64(0.5)]);
331 assert!(
332 outer
333 .iter()
334 .any(|p| p.curve.evaluate(S::ONE).unwrap().could_be_equal(&far)),
335 "no quarter piece ends at the right apex"
336 );
337 }
338 #[test]
339 fn slot_with_hole_regions() {
340 for_all_scalars!(check_slot_with_hole_regions);
341 }
342
343 #[test]
346 fn nested_squares_alternate() {
347 let mut s = Sketch::new();
348 for (size, clockwise) in [(3.0, true), (2.0, false), (1.0, true)] {
349 let h = size / 2.0;
350 let mut corners = [[-h, -h], [h, -h], [h, h], [-h, h]];
351 if clockwise {
352 corners.reverse();
353 }
354 let p: Vec<PointId> = corners.iter().map(|c| s.add_point(c[0], c[1])).collect();
355 for i in 0..4 {
356 s.add_line(p[i], p[(i + 1) % 4]);
357 }
358 }
359 let regions = s.regions().unwrap();
360 assert_eq!(regions.len(), 2);
361 let with_hole = regions.iter().filter(|r| r.holes.len() == 1).count();
362 assert_eq!(with_hole, 1);
363 let outermost = regions.iter().find(|r| r.holes.len() == 1).unwrap();
365 assert!(outermost.outer.edges.iter().all(|e| e.reversed));
366 }
367
368 #[test]
374 fn nesting_resolves_a_gap_finer_than_any_sampling() {
375 let mut s = Sketch::new();
376 let outer = s.add_point(0.0, 0.0);
377 s.add_circle(outer, 1.0);
378 let inner = s.add_point(0.0, 0.0);
379 s.add_circle(inner, 1.0 - 5e-5);
380
381 let regions = s.regions().unwrap();
382 assert_eq!(regions.len(), 1, "one region: the ring between them");
383 assert_eq!(regions[0].holes.len(), 1);
384 assert!(!regions[0].outer.edges[0].reversed);
387 assert!(regions[0].holes[0].edges[0].reversed);
388 }
389
390 #[test]
394 fn regions_of_a_large_sketch_are_found_quickly() {
395 let mut s = Sketch::new();
396 for radius in [3.0, 1.0] {
397 let p: Vec<PointId> = (0..20)
398 .map(|k| {
399 let a = std::f64::consts::TAU * k as f64 / 20.0;
400 s.add_point(radius * a.cos(), radius * a.sin())
401 })
402 .collect();
403 for i in 0..20 {
404 s.add_line(p[i], p[(i + 1) % 20]);
405 }
406 }
407 let start = std::time::Instant::now();
408 let regions = s.regions().unwrap();
409 let elapsed = start.elapsed();
410 assert_eq!(regions.len(), 1);
411 assert_eq!(regions[0].holes.len(), 1);
412 assert_eq!(regions[0].outer.edges.len(), 20);
413 assert!(
414 elapsed < std::time::Duration::from_millis(500),
415 "finding the regions of 40 curves took {elapsed:?}"
416 );
417 }
418
419 #[test]
420 fn branching_profile_is_rejected() {
421 let mut s = Sketch::new();
422 let p: Vec<PointId> = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
423 .iter()
424 .map(|c| s.add_point(c[0], c[1]))
425 .collect();
426 for i in 0..4 {
427 s.add_line(p[i], p[(i + 1) % 4]);
428 }
429 s.add_line(p[0], p[2]);
430 assert!(s.regions().is_err());
431 }
432
433 #[test]
435 fn arc_from_curvature() {
436 let mut s = Sketch::new();
437 let a = s.add_point(1.0, 0.0);
438 let b = s.add_point(0.0, 1.0);
439 let arc = s.add_arc(a, b, 1.0);
440 let CurveKind::Arc { sweep, .. } = s.curves[&arc].kind else {
441 unreachable!()
442 };
443 assert!(close(sweep, FRAC_PI_2));
444 }
445
446 #[test]
448 fn spline_tangent_to_arc() {
449 let mut s = Sketch::new();
450 let a = s.add_point(0.0, 0.0);
451 let b = s.add_point(1.0, 0.5);
452 let c = s.add_point(2.0, -0.3);
453 let d = s.add_point(3.0, 0.0);
454 let spline = s.add_spline(vec![a, b, c, d]);
455 let arc = s.add_arc_with_sweep(d, a, 2.5);
456 s.constrain(Constraint::Tangent { a: spline, b: arc });
457 s.constrain(Constraint::Fix {
458 point: a,
459 x: 0.0,
460 y: 0.0,
461 });
462 s.constrain(Constraint::Fix {
463 point: d,
464 x: 3.0,
465 y: 0.0,
466 });
467 s.constrain(Constraint::Fix {
468 point: b,
469 x: 1.0,
470 y: 0.5,
471 });
472 let report = s.solve().unwrap();
473 assert!(report.converged, "{report:?}");
474 let regions = s.regions().unwrap();
475 assert_eq!(regions.len(), 1);
476 }
477
478 #[test]
483 fn profile_pieces_name_their_sketch_origin() {
484 let mut s = Sketch::new();
485 let c = s.add_point(0.0, 0.0);
486 let circle = s.add_circle(c, 1.0);
487 let p: Vec<PointId> = [[-2.0, -2.0], [2.0, -2.0], [2.0, 2.0], [-2.0, 2.0]]
488 .iter()
489 .map(|c| s.add_point(c[0], c[1]))
490 .collect();
491 let lines: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
492
493 let regions = s.regions().unwrap();
494 let positions = s.positions();
495 let outer = regions[0]
496 .outer
497 .to_nurbs::<ScalInF64>(&s, &positions)
498 .unwrap();
499 let names: Vec<String> = outer.iter().map(|p| p.name()).collect();
500 assert_eq!(names.len(), 4);
501 for l in &lines {
502 assert!(names.contains(&format!("{l}")), "{names:?}");
503 }
504 assert!(
505 outer
506 .iter()
507 .all(|piece| matches!(piece.start, ProfileJoint::Point(_)))
508 );
509
510 let hole = regions[0].holes[0]
512 .to_nurbs::<ScalInF64>(&s, &positions)
513 .unwrap();
514 let joints: Vec<String> = hole.iter().map(|p| p.start.to_string()).collect();
515 assert_eq!(
516 joints,
517 [
518 format!("{circle}@0"),
519 format!("{circle}@3"),
520 format!("{circle}@2"),
521 format!("{circle}@1")
522 ]
523 );
524 let names: Vec<String> = hole.iter().map(|p| p.name()).collect();
525 assert_eq!(
526 names,
527 [
528 format!("{circle}#3"),
529 format!("{circle}#2"),
530 format!("{circle}#1"),
531 format!("{circle}")
532 ]
533 );
534 }
535
536 #[test]
539 fn sketch_json_round_trip_keeps_ids() {
540 let mut s = Sketch::new();
541 let a = s.add_point(0.0, 0.0);
542 let b = s.add_point(1.0, 0.0);
543 let l = s.add_line(a, b);
544 s.constrain(Constraint::Horizontal { line: l });
545 let json = serde_json::to_string(&s).unwrap();
546 assert!(json.contains(&format!("\"{}\":", l.0)), "{json}");
547 let back: Sketch = serde_json::from_str(&json).unwrap();
548 assert_eq!(back, s);
549 back.validate().unwrap();
550 }
551}