1pub mod copies;
21pub mod dimension;
22pub mod geometry;
23pub mod offset;
24pub mod plain;
25pub mod profile;
26pub mod sketch;
27pub mod solve;
28pub mod space;
29pub mod text;
30mod variables;
31
32pub use profile::{ProfileEdge, ProfileJoint, ProfileLoop, ProfilePiece, Region, Shape};
33pub use sketch::{
34 Constraint, ConstraintId, Curve, CurveId, CurveKind, Enclosure, Point, PointId, Positions,
35 Sketch, SplineShape,
36};
37pub use solve::SolveReport;
38
39#[cfg(test)]
40mod tests {
41 use super::*;
42 use crate::plain::xy;
43 use geop_core_math::{scalars::Scalar, vector::Vector2};
44 use std::f64::consts::{FRAC_PI_2, PI};
45
46 fn close(a: f64, b: f64) -> bool {
47 (a - b).abs() < 1e-7
48 }
49
50 fn n(x: f64) -> Scalar {
51 Scalar::from_f64(x)
52 }
53
54 fn v2(p: [f64; 2]) -> Vector2 {
55 Vector2::from_array(p.map(Scalar::from_f64))
56 }
57
58 fn sloppy_rectangle() -> (Sketch, [PointId; 4]) {
61 let mut s = Sketch::new();
62 let p = [
63 s.add_point(n(0.1), n(-0.1)),
64 s.add_point(n(2.2), n(0.2)),
65 s.add_point(n(1.9), n(1.3)),
66 s.add_point(n(-0.2), n(0.8)),
67 ];
68 let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
69 s.constrain(Constraint::Fix {
70 point: p[0],
71 x: n(0.0),
72 y: n(0.0),
73 });
74 s.constrain(Constraint::Horizontal { line: l[0] });
75 s.constrain(Constraint::Horizontal { line: l[2] });
76 s.constrain(Constraint::Vertical { line: l[1] });
77 s.constrain(Constraint::Vertical { line: l[3] });
78 s.constrain(Constraint::Length {
79 curve: l[0],
80 value: n(2.0),
81 });
82 s.constrain(Constraint::Distance {
83 a: p[1],
84 b: p[2],
85 value: n(1.0),
86 });
87 (s, p)
88 }
89
90 #[test]
93 fn rectangle_is_solved_and_fully_constrained() {
94 let (mut s, p) = sloppy_rectangle();
95
96 let report = s.solve().unwrap();
97 assert!(report.converged, "{report:?}");
98 assert_eq!(report.dof, 0, "{report:?}");
99 assert!(report.free_points.values().all(|f| !f), "{report:?}");
100 let expect = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]];
101 for (pi, e) in p.iter().zip(expect) {
102 let q = xy(&s, *pi);
103 assert!(close(q[0], e[0]) && close(q[1], e[1]), "{q:?} vs {e:?}");
104 }
105 }
106
107 #[test]
110 fn unanchored_rectangle_has_two_dof() {
111 let mut s = Sketch::new();
112 let p = [
113 s.add_point(n(0.0), n(0.0)),
114 s.add_point(n(2.0), n(0.1)),
115 s.add_point(n(2.0), n(1.0)),
116 s.add_point(n(0.0), n(1.0)),
117 ];
118 let l: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
119 s.constrain(Constraint::Horizontal { line: l[0] });
120 s.constrain(Constraint::Horizontal { line: l[2] });
121 s.constrain(Constraint::Vertical { line: l[1] });
122 s.constrain(Constraint::Vertical { line: l[3] });
123 let report = s.solve().unwrap();
124 assert!(report.converged);
125 assert_eq!(report.dof, 4, "width, height and translation");
126 assert!(report.free_points.values().all(|f| *f));
127 }
128
129 #[test]
132 fn coincident_points_merge() {
133 let mut s = Sketch::new();
134 let a = s.add_point(n(0.0), n(0.0));
135 let b = s.add_point(n(1.0), n(0.0));
136 let c = s.add_point(n(1.1), n(0.05));
137 let d = s.add_point(n(1.5), n(1.0));
138 s.add_line(a, b);
139 s.add_line(c, d);
140 s.constrain(Constraint::Coincident { a: b, b: c });
141 let report = s.solve().unwrap();
142 assert!(report.converged);
143 assert_eq!(xy(&s, b), xy(&s, c));
144 assert_eq!(report.dof, 6);
145 }
146
147 #[test]
150 fn line_arc_tangent_at_shared_endpoint() {
151 let mut s = Sketch::new();
152 let a = s.add_point(n(0.0), n(0.0));
153 let b = s.add_point(n(2.0), n(0.0));
154 let c = s.add_point(n(2.0), n(1.0));
155 let line = s.add_line(a, b);
156 let arc = s.add_arc(b, c, n(2.0 * 0.75f64.asin()));
157 s.constrain(Constraint::Fix {
158 point: a,
159 x: n(0.0),
160 y: n(0.0),
161 });
162 s.constrain(Constraint::Horizontal { line });
163 s.constrain(Constraint::Length {
164 curve: line,
165 value: n(2.0),
166 });
167 s.constrain(Constraint::Tangent { a: line, b: arc });
168 s.constrain(Constraint::Radius {
169 curve: arc,
170 value: n(0.5),
171 });
172 s.constrain(Constraint::Fix {
173 point: c,
174 x: n(2.0),
175 y: n(1.0),
176 });
177 let report = s.solve().unwrap();
178 assert!(report.converged, "{report:?}");
179 let CurveKind::Arc { sweep, .. } = s.curves[&arc].kind else {
180 unreachable!()
181 };
182 assert!(close(sweep.to_f64(), PI), "sweep {sweep:?}");
184 assert_eq!(report.dof, 0, "{report:?}");
185 }
186
187 #[test]
190 fn circle_tangent_to_lines() {
191 let mut s = Sketch::new();
192 let o = s.add_point(n(0.0), n(0.0));
193 let x = s.add_point(n(3.0), n(0.0));
194 let y = s.add_point(n(0.0), n(3.0));
195 let lx = s.add_line(o, x);
196 let ly = s.add_line(o, y);
197 let c = s.add_point(n(0.8), n(1.3));
198 let circle = s.add_circle(c, n(0.7));
199 for (p, xy) in [(o, [0.0, 0.0]), (x, [3.0, 0.0]), (y, [0.0, 3.0])] {
200 s.constrain(Constraint::Fix {
201 point: p,
202 x: n(xy[0]),
203 y: n(xy[1]),
204 });
205 }
206 s.constrain(Constraint::Tangent { a: lx, b: circle });
207 s.constrain(Constraint::Tangent { a: circle, b: ly });
208 s.constrain(Constraint::Radius {
209 curve: circle,
210 value: n(1.0),
211 });
212 let report = s.solve().unwrap();
213 assert!(report.converged, "{report:?}");
214 let q = xy(&s, c);
215 assert!(close(q[0], 1.0) && close(q[1], 1.0), "{q:?}");
216 }
217
218 #[test]
220 fn conflicting_constraints_do_not_converge() {
221 let mut s = Sketch::new();
222 let a = s.add_point(n(0.0), n(0.0));
223 let b = s.add_point(n(1.0), n(0.0));
224 let l = s.add_line(a, b);
225 s.constrain(Constraint::Length {
226 curve: l,
227 value: n(1.0),
228 });
229 s.constrain(Constraint::Distance {
230 a,
231 b,
232 value: n(2.0),
233 });
234 let report = s.solve().unwrap();
235 assert!(!report.converged);
236 assert!(!report.failed_constraints.is_empty());
237 }
238
239 #[test]
242 fn drag_follows_cursor_only_where_free() {
243 let mut s = Sketch::new();
244 let a = s.add_point(n(0.0), n(0.0));
245 let b = s.add_point(n(1.0), n(0.0));
246 let l = s.add_line(a, b);
247 s.constrain(Constraint::Fix {
248 point: a,
249 x: n(0.0),
250 y: n(0.0),
251 });
252 s.constrain(Constraint::Length {
253 curve: l,
254 value: n(1.0),
255 });
256 let report = s.solve_with_drag(&[(b, v2([0.0, 3.0]))]).unwrap();
257 assert!(report.converged, "{report:?}");
258 let q = xy(&s, b);
259 assert!(close(q[0], 0.0) && close(q[1], 1.0), "{q:?}: {report:?}");
260
261 let report = s.solve_with_drag(&[(a, v2([5.0, 5.0]))]).unwrap();
262 assert!(report.converged);
263 let q = xy(&s, a);
264 assert!(close(q[0], 0.0) && close(q[1], 0.0), "{q:?}");
265 }
266
267 #[test]
270 fn equilateral_triangle_from_symmetry_and_angle() {
271 let mut s = Sketch::new();
272 let a = s.add_point(n(-1.0), n(0.1));
273 let b = s.add_point(n(1.2), n(-0.1));
274 let c = s.add_point(n(0.1), n(1.5));
275 let base = s.add_line(a, b);
276 let left = s.add_line(c, a);
277 let right = s.add_line(c, b);
278 let m = s.add_point(n(0.0), n(0.0));
279 let axis_top = s.add_point(n(0.0), n(2.0));
280 let axis = s.add_line(m, axis_top);
281 s.set_construction(axis, true);
282 s.constrain(Constraint::Fix {
283 point: m,
284 x: n(0.0),
285 y: n(0.0),
286 });
287 s.constrain(Constraint::Vertical { line: axis });
288 s.constrain(Constraint::Midpoint {
289 point: m,
290 curve: base,
291 });
292 s.constrain(Constraint::Symmetric { a, b, line: axis });
293 s.constrain(Constraint::PointOnCurve {
294 point: c,
295 curve: axis,
296 });
297 s.constrain(Constraint::Equal { a: left, b: right });
298 s.constrain(Constraint::Angle {
299 a: left,
300 b: right,
301 value: n(PI / 3.0),
302 });
303 s.constrain(Constraint::Length {
304 curve: base,
305 value: n(2.0),
306 });
307 let report = s.solve().unwrap();
308 assert!(report.converged, "{report:?}");
309 let q = xy(&s, c);
310 assert!(close(q[0], 0.0) && close(q[1], 3f64.sqrt()), "{q:?}");
311 }
312
313 #[test]
317 fn slot_with_hole_regions() {
318 let mut s = Sketch::new();
319 let p = [
320 s.add_point(n(0.0), n(0.0)),
321 s.add_point(n(2.0), n(0.0)),
322 s.add_point(n(2.0), n(1.0)),
323 s.add_point(n(0.0), n(1.0)),
324 ];
325 s.add_line(p[0], p[1]);
326 s.add_arc(p[1], p[2], n(PI));
327 s.add_line(p[2], p[3]);
328 s.add_arc(p[3], p[0], n(PI));
329 let c = s.add_point(n(1.0), n(0.5));
330 s.add_circle(c, n(0.25));
331 let q = s.add_point(n(-1.0), n(-1.0));
333 s.add_line(p[0], q);
334
335 let regions = s.regions().unwrap();
336 assert_eq!(regions.len(), 1);
337 assert_eq!(regions[0].outer.edges.len(), 4);
338 assert_eq!(regions[0].holes.len(), 1);
339
340 let geometry = s.enclose().unwrap();
341 for (lp, count) in [(®ions[0].outer, 6), (®ions[0].holes[0], 4)] {
342 let pieces = lp.to_nurbs(&s, &geometry).unwrap();
343 let curves: Vec<_> = pieces.iter().map(|p| &p.curve).collect();
344 assert_eq!(curves.len(), count);
345 for (i, piece) in pieces.iter().enumerate() {
347 assert_eq!(piece.end, pieces[(i + 1) % pieces.len()].start);
348 }
349 for (i, c) in curves.iter().enumerate() {
350 let (t0, t1) = c.domain();
351 assert!(t0.could_be_equal(Scalar::ZERO) && t1.could_be_equal(Scalar::ONE));
352 let next = curves[(i + 1) % curves.len()];
353 let end = c.evaluate(Scalar::ONE).unwrap();
354 let start = next.evaluate(Scalar::ZERO).unwrap();
355 assert!(end.could_be_equal(&start), "{end:?} vs {start:?}");
356 }
357 }
358 let outer = regions[0].outer.to_nurbs(&s, &geometry).unwrap();
360 let far = Vector2::from_array([Scalar::from_f64(2.5), Scalar::from_f64(0.5)]);
361 assert!(
362 outer
363 .iter()
364 .any(|p| p.curve.evaluate(Scalar::ONE).unwrap().could_be_equal(&far)),
365 "no quarter piece ends at the right apex: {:?}",
366 outer
367 .iter()
368 .map(|p| p.curve.evaluate(Scalar::ONE).unwrap())
369 .collect::<Vec<_>>()
370 );
371 }
372
373 #[test]
376 fn nested_squares_alternate() {
377 let mut s = Sketch::new();
378 for (size, clockwise) in [(3.0, true), (2.0, false), (1.0, true)] {
379 let h = size / 2.0;
380 let mut corners = [[-h, -h], [h, -h], [h, h], [-h, h]];
381 if clockwise {
382 corners.reverse();
383 }
384 let p: Vec<PointId> = corners
385 .iter()
386 .map(|c| s.add_point(n(c[0]), n(c[1])))
387 .collect();
388 for i in 0..4 {
389 s.add_line(p[i], p[(i + 1) % 4]);
390 }
391 }
392 let regions = s.regions().unwrap();
393 assert_eq!(regions.len(), 2);
394 let with_hole = regions.iter().filter(|r| r.holes.len() == 1).count();
395 assert_eq!(with_hole, 1);
396 let outermost = regions.iter().find(|r| r.holes.len() == 1).unwrap();
398 assert!(outermost.outer.edges.iter().all(|e| e.reversed));
399 }
400
401 #[test]
407 fn nesting_resolves_a_gap_finer_than_any_sampling() {
408 let mut s = Sketch::new();
409 let outer = s.add_point(n(0.0), n(0.0));
410 s.add_circle(outer, n(1.0));
411 let inner = s.add_point(n(0.0), n(0.0));
412 s.add_circle(inner, n(1.0 - 5e-5));
413
414 let regions = s.regions().unwrap();
415 assert_eq!(regions.len(), 1, "one region: the ring between them");
416 assert_eq!(regions[0].holes.len(), 1);
417 assert!(!regions[0].outer.edges[0].reversed);
420 assert!(regions[0].holes[0].edges[0].reversed);
421 }
422
423 #[test]
427 fn regions_of_a_large_sketch_are_found_quickly() {
428 let mut s = Sketch::new();
429 for radius in [3.0, 1.0] {
430 let p: Vec<PointId> = (0..20)
431 .map(|k| {
432 let a = std::f64::consts::TAU * k as f64 / 20.0;
433 s.add_point(n(radius * a.cos()), n(radius * a.sin()))
434 })
435 .collect();
436 for i in 0..20 {
437 s.add_line(p[i], p[(i + 1) % 20]);
438 }
439 }
440 let start = std::time::Instant::now();
441 let regions = s.regions().unwrap();
442 let elapsed = start.elapsed();
443 assert_eq!(regions.len(), 1);
444 assert_eq!(regions[0].holes.len(), 1);
445 assert_eq!(regions[0].outer.edges.len(), 20);
446 assert!(
447 elapsed < std::time::Duration::from_millis(500),
448 "finding the regions of 40 curves took {elapsed:?}"
449 );
450 }
451
452 #[test]
453 fn branching_profile_is_rejected() {
454 let mut s = Sketch::new();
455 let p: Vec<PointId> = [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]
456 .iter()
457 .map(|c| s.add_point(n(c[0]), n(c[1])))
458 .collect();
459 for i in 0..4 {
460 s.add_line(p[i], p[(i + 1) % 4]);
461 }
462 s.add_line(p[0], p[2]);
463 assert!(s.regions().is_err());
464 }
465
466 #[test]
468 fn arc_from_curvature() {
469 let mut s = Sketch::new();
470 let a = s.add_point(n(1.0), n(0.0));
471 let b = s.add_point(n(0.0), n(1.0));
472 let arc = s.add_arc(a, b, n(FRAC_PI_2));
473 let CurveKind::Arc { sweep, .. } = s.curves[&arc].kind else {
474 unreachable!()
475 };
476 assert!(close(sweep.to_f64(), FRAC_PI_2));
477 }
478
479 #[test]
481 fn spline_tangent_to_arc() {
482 let mut s = Sketch::new();
483 let a = s.add_point(n(0.0), n(0.0));
484 let b = s.add_point(n(1.0), n(0.5));
485 let c = s.add_point(n(2.0), n(-0.3));
486 let d = s.add_point(n(3.0), n(0.0));
487 let spline = s.add_spline(vec![a, b, c, d]);
488 let arc = s.add_arc(d, a, n(2.5));
489 s.constrain(Constraint::Tangent { a: spline, b: arc });
490 s.constrain(Constraint::Fix {
491 point: a,
492 x: n(0.0),
493 y: n(0.0),
494 });
495 s.constrain(Constraint::Fix {
496 point: d,
497 x: n(3.0),
498 y: n(0.0),
499 });
500 s.constrain(Constraint::Fix {
501 point: b,
502 x: n(1.0),
503 y: n(0.5),
504 });
505 let report = s.solve().unwrap();
506 assert!(report.converged, "{report:?}");
507 let regions = s.regions().unwrap();
508 assert_eq!(regions.len(), 1);
509 }
510
511 #[test]
516 fn profile_pieces_name_their_sketch_origin() {
517 let mut s = Sketch::new();
518 let c = s.add_point(n(0.0), n(0.0));
519 let circle = s.add_circle(c, n(1.0));
520 let p: Vec<PointId> = [[-2.0, -2.0], [2.0, -2.0], [2.0, 2.0], [-2.0, 2.0]]
521 .iter()
522 .map(|c| s.add_point(n(c[0]), n(c[1])))
523 .collect();
524 let lines: Vec<CurveId> = (0..4).map(|i| s.add_line(p[i], p[(i + 1) % 4])).collect();
525
526 let regions = s.regions().unwrap();
527 let geometry = s.enclose().unwrap();
528 let outer = regions[0].outer.to_nurbs(&s, &geometry).unwrap();
529 let names: Vec<String> = outer.iter().map(|p| p.name()).collect();
530 assert_eq!(names.len(), 4);
531 for l in &lines {
532 assert!(names.contains(&format!("{l}")), "{names:?}");
533 }
534 assert!(
535 outer
536 .iter()
537 .all(|piece| matches!(piece.start, ProfileJoint::Point(_)))
538 );
539
540 let hole = regions[0].holes[0].to_nurbs(&s, &geometry).unwrap();
542 let joints: Vec<String> = hole.iter().map(|p| p.start.to_string()).collect();
543 assert_eq!(
544 joints,
545 [
546 format!("{circle}@0"),
547 format!("{circle}@3"),
548 format!("{circle}@2"),
549 format!("{circle}@1")
550 ]
551 );
552 let names: Vec<String> = hole.iter().map(|p| p.name()).collect();
553 assert_eq!(
554 names,
555 [
556 format!("{circle}#3"),
557 format!("{circle}#2"),
558 format!("{circle}#1"),
559 format!("{circle}")
560 ]
561 );
562 }
563
564 #[test]
567 fn sketch_json_round_trip_keeps_ids() {
568 let mut s = Sketch::new();
569 let a = s.add_point(n(0.0), n(0.0));
570 let b = s.add_point(n(1.0), n(0.0));
571 let l = s.add_line(a, b);
572 s.constrain(Constraint::Horizontal { line: l });
573 let json = serde_json::to_string(&s).unwrap();
574 assert!(json.contains(&format!("\"{}\":", l.0)), "{json}");
575 let back: Sketch = serde_json::from_str(&json).unwrap();
576 assert_eq!(back, s);
577 back.validate().unwrap();
578 }
579
580 #[test]
583 fn remove_takes_what_depends_on_it() {
584 let mut s = Sketch::new();
585 let a = s.add_point(n(0.0), n(0.0));
586 let b = s.add_point(n(1.0), n(0.0));
587 let c = s.add_point(n(1.0), n(1.0));
588 let lone = s.add_point(n(5.0), n(5.0));
589 let ab = s.add_line(a, b);
590 let bc = s.add_line(b, c);
591 let horizontal = s.constrain(Constraint::Horizontal { line: ab });
592 let vertical = s.constrain(Constraint::Vertical { line: bc });
593 let fix = s.constrain(Constraint::Fix {
594 point: a,
595 x: n(0.0),
596 y: n(0.0),
597 });
598 s.remove(&[], &[ab], &[]);
599 assert!(!s.curves.contains_key(&ab) && s.curves.contains_key(&bc));
600 assert!(!s.constraints.contains_key(&horizontal));
601 assert!(s.constraints.contains_key(&vertical) && s.constraints.contains_key(&fix));
602 assert!(s.points.contains_key(&a) && s.points.contains_key(&b));
604
605 s.remove(&[], &[], &[fix]);
606 assert!(!s.points.contains_key(&a), "a was only the fix's");
607 s.remove(&[c], &[], &[]);
608 assert!(s.curves.is_empty() && s.constraints.is_empty());
609 assert_eq!(s.points.keys().copied().collect::<Vec<_>>(), [lone]);
610 s.validate().unwrap();
611 }
612
613 #[test]
618 fn solutions_are_enclosed() {
619 let (mut s, p) = sloppy_rectangle();
620 assert!(s.solve().unwrap().converged);
621 let enclosed = s.enclose().unwrap();
622 let exact = [[0.0, 0.0], [2.0, 0.0], [2.0, 1.0], [0.0, 1.0]];
623 for (pi, e) in p.iter().zip(exact) {
624 let q = enclosed.points[pi];
625 for k in 0..2 {
626 assert!(
627 q[k].could_be_equal(Scalar::from_f64(e[k])),
628 "{q:?} vs {e:?}"
629 );
630 assert!(q[k].width().to_f64() < 1e-12, "{q:?}");
631 }
632 }
633 }
634
635 #[test]
639 fn half_circle_tangent_to_parallel_sides_is_enclosed() {
640 let mut s = Sketch::new();
641 let p = [
642 s.add_point(n(-0.8559366928294662), n(0.6532261597610688)),
643 s.add_point(n(0.6690494426064505), n(0.6532261597610689)),
644 s.add_point(n(0.6690494426064503), n(-0.40501734890700397)),
645 s.add_point(n(-0.8559366928294662), n(-0.405017348907004)),
646 ];
647 let top = s.add_line(p[0], p[1]);
648 let end = s.add_arc(p[1], p[2], n(-PI));
649 let bottom = s.add_line(p[2], p[3]);
650 let side = s.add_line(p[3], p[0]);
651 s.constrain(Constraint::Horizontal { line: top });
652 s.constrain(Constraint::Tangent { a: top, b: end });
653 s.constrain(Constraint::Horizontal { line: bottom });
654 s.constrain(Constraint::Tangent { a: end, b: bottom });
655 s.constrain(Constraint::Vertical { line: side });
656 assert!(s.solve().unwrap().converged);
657 s.enclose().unwrap();
658 }
659
660 #[test]
663 fn free_choices_stay_sharp() {
664 let mut s = Sketch::new();
665 let a = s.add_point(n(0.3), n(0.7));
666 let b = s.add_point(n(1.9), n(0.75));
667 let line = s.add_line(a, b);
668 s.constrain(Constraint::Horizontal { line });
669 assert!(s.solve().unwrap().converged);
670 let enclosed = s.enclose().unwrap();
671 let sharp = [enclosed.points[&a], enclosed.points[&b]]
673 .iter()
674 .flat_map(|q| [q[0], q[1]])
675 .filter(|v| v.is_sharp())
676 .count();
677 assert!(sharp >= 3, "{enclosed:?}");
678 assert!(enclosed.points[&a][1].could_be_equal(enclosed.points[&b][1]));
679 s.constrain(Constraint::Vertical { line });
681 s.constrain(Constraint::Fix {
682 point: a,
683 x: n(0.0),
684 y: n(0.0),
685 });
686 s.constrain(Constraint::Fix {
687 point: b,
688 x: n(1.0),
689 y: n(0.0),
690 });
691 let report = s.solve().unwrap();
692 assert!(!report.converged);
693 assert_eq!(s.enclose().unwrap(), Enclosure::as_drawn(&s));
694 }
695
696 #[test]
700 fn fixed_points_stay_where_given() {
701 let mut s = Sketch::new();
702 let free = s.add_point(n(0.2), n(0.1));
703 let end = s.add_point(n(2.0), n(0.3));
704 let anchor = s.add_fixed_point(n(1.0), n(1.0));
705 s.add_line(anchor, end);
706 s.constrain(Constraint::Coincident { a: free, b: anchor });
707 let line = s.add_line(free, end);
708 s.constrain(Constraint::Length {
709 curve: line,
710 value: n(1.0),
711 });
712 let report = s.solve().unwrap();
713 assert!(report.converged, "{report:?}");
714 assert_eq!(xy(&s, anchor), [1.0, 1.0]);
715 assert_eq!(xy(&s, free), [1.0, 1.0]);
716 let e = xy(&s, end);
717 assert!(close((e[0] - 1.0).hypot(e[1] - 1.0), 1.0), "{e:?}");
718 assert_eq!(report.dof, 1, "{report:?}");
720 assert!(!report.free_points[&anchor] && report.free_points[&end]);
721 let enclosed = s.enclose().unwrap();
722 assert!(enclosed.points[&anchor][0].is_sharp());
723 }
724
725 #[test]
728 fn coincident_fixed_points_are_checked_not_merged() {
729 let mut s = Sketch::new();
730 let a = s.add_fixed_point(n(0.0), n(0.0));
731 let b = s.add_fixed_point(n(1.0), n(0.0));
732 let k = s.constrain(Constraint::Coincident { a, b });
733 let report = s.solve().unwrap();
734 assert_eq!(report.failed_constraints, vec![k]);
735 assert_eq!(xy(&s, b), [1.0, 0.0]);
736 }
737
738 #[test]
741 fn fixed_curves_keep_their_parameter() {
742 let mut s = Sketch::new();
743 let c = s.add_fixed_point(n(0.0), n(0.0));
744 let circle = s.add_circle(c, n(2.0));
745 s.curves.get_mut(&circle).unwrap().fixed = true;
746 s.validate().unwrap();
747 let k = s.constrain(Constraint::Diameter {
748 curve: circle,
749 value: n(3.0),
750 });
751 let report = s.solve().unwrap();
752 assert_eq!(report.failed_constraints, vec![k]);
753 let CurveKind::Circle { radius, .. } = s.curves[&circle].kind else {
754 unreachable!()
755 };
756 assert_eq!(radius.to_f64(), 2.0);
757 let free = s.add_point(n(5.0), n(0.0));
759 let other = s.add_circle(free, n(1.0));
760 s.curves.get_mut(&other).unwrap().fixed = true;
761 assert!(s.validate().is_err());
762 }
763
764 #[test]
766 fn diameter_sizes_circles_and_arcs() {
767 let mut s = Sketch::new();
768 let c = s.add_point(n(0.0), n(0.0));
769 let circle = s.add_circle(c, n(0.7));
770 s.constrain(Constraint::Diameter {
771 curve: circle,
772 value: n(3.0),
773 });
774 let a = s.add_point(n(5.0), n(0.0));
775 let b = s.add_point(n(7.0), n(0.0));
776 let arc = s.add_arc(a, b, n(1.0));
777 s.constrain(Constraint::Fix {
778 point: a,
779 x: n(5.0),
780 y: n(0.0),
781 });
782 s.constrain(Constraint::Fix {
783 point: b,
784 x: n(7.0),
785 y: n(0.0),
786 });
787 s.constrain(Constraint::Diameter {
788 curve: arc,
789 value: n(4.0),
790 });
791 let report = s.solve().unwrap();
792 assert!(report.converged, "{report:?}");
793 let CurveKind::Circle { radius, .. } = s.curves[&circle].kind else {
794 unreachable!()
795 };
796 assert!(close(radius.to_f64(), 1.5));
797 let CurveKind::Arc { sweep, .. } = s.curves[&arc].kind else {
798 unreachable!()
799 };
800 assert!(close(sweep.to_f64(), PI / 3.0), "{sweep:?}");
802 }
803
804 #[test]
807 fn rational_splines_are_exact() {
808 let mut s = Sketch::new();
809 let p = [
810 s.add_point(n(1.0), n(0.0)),
811 s.add_point(n(1.0), n(1.0)),
812 s.add_point(n(0.0), n(1.0)),
813 ];
814 let spline = s.add_curve(CurveKind::Spline {
815 control_points: p.to_vec(),
816 shape: Some(SplineShape {
817 degree: 2,
818 knots: [0.0, 0.0, 0.0, 2.0, 2.0, 2.0].map(n).to_vec(),
819 weights: [1.0, 0.5f64.sqrt(), 1.0].map(n).to_vec(),
820 }),
821 });
822 s.validate().unwrap();
823 for q in profile::curve_polyline(&s, spline).unwrap() {
824 let r = q[0].to_f64().hypot(q[1].to_f64());
825 assert!(close(r, 1.0), "{q:?}");
826 }
827 }
828
829 #[test]
832 fn center_follows_the_arc() {
833 let mut s = Sketch::new();
834 let c = s.add_point(n(0.3), n(0.2));
835 let a = s.add_point(n(1.0), n(0.0));
836 let b = s.add_point(n(0.0), n(1.0));
837 let arc = s.add_arc(a, b, n(FRAC_PI_2));
838 s.constrain(Constraint::Center {
839 point: c,
840 curve: arc,
841 });
842 for (p, at) in [(a, [2.0, 0.0]), (b, [0.0, 2.0])] {
843 s.constrain(Constraint::Fix {
844 point: p,
845 x: n(at[0]),
846 y: n(at[1]),
847 });
848 }
849 s.constrain(Constraint::Radius {
850 curve: arc,
851 value: n(2.0),
852 });
853 let report = s.solve().unwrap();
854 assert!(report.converged, "{report:?}");
855 let q = xy(&s, c);
856 assert!(close(q[0], 0.0) && close(q[1], 0.0), "{q:?}");
857 }
858
859 #[test]
863 fn letters_are_areas_with_their_holes() {
864 let mut s = Sketch::new();
865 let anchor = s.add_point(n(3.0), n(-2.0));
866 let text = s.add_text(anchor, "BoT", n(5.0), n(0.2));
867 let regions = s.regions().unwrap();
868 let mut holes: Vec<usize> = regions.iter().map(|r| r.holes.len()).collect();
869 holes.sort();
870 assert_eq!(holes, [0, 1, 2], "{regions:?}");
871 let geometry = s.enclose().unwrap();
872 let mut names = Vec::new();
873 let mut joints = Vec::new();
874 for lp in regions
875 .iter()
876 .flat_map(|r| std::iter::once(&r.outer).chain(&r.holes))
877 {
878 let pieces = lp.to_nurbs(&s, &geometry).unwrap();
879 for (k, p) in pieces.iter().enumerate() {
880 assert_eq!(p.source, text);
881 assert_eq!(p.end, pieces[(k + 1) % pieces.len()].start, "{lp:?}");
882 names.push(p.name());
883 joints.push(p.start);
884 }
885 }
886 let count = names.len();
887 names.sort();
888 names.dedup();
889 joints.sort();
890 joints.dedup();
891 assert_eq!((names.len(), joints.len()), (count, count));
892 }
893
894 #[test]
898 fn a_letter_inside_a_frame_is_a_hole_in_it() {
899 let mut s = Sketch::new();
900 let corners = [[-1.0, -2.0], [5.0, -2.0], [5.0, 6.0], [-1.0, 6.0]];
901 let p: Vec<PointId> = corners.iter().map(|&c| s.new_point_at(c)).collect();
902 for k in 0..4 {
903 s.add_line(p[k], p[(k + 1) % 4]);
904 }
905 let anchor = s.add_point(n(0.0), n(0.0));
906 s.add_text(anchor, "o", n(4.0), n(0.0));
907 let regions = s.regions().unwrap();
908 let mut holes: Vec<usize> = regions.iter().map(|r| r.holes.len()).collect();
909 holes.sort();
910 assert_eq!(holes, [0, 1], "{regions:?}");
911 }
912
913 #[test]
916 fn a_text_follows_its_anchor() {
917 let mut s = Sketch::new();
918 let anchor = s.add_point(n(1.0), n(1.0));
919 let text = s.add_text(anchor, "l", n(2.0), n(0.0));
920 let low = |s: &Sketch| {
921 let lines = profile::curve_polylines(s, text).unwrap();
922 lines
923 .iter()
924 .flatten()
925 .map(|q| q[1].to_f64())
926 .fold(f64::INFINITY, f64::min)
927 };
928 assert!(close(low(&s), 1.0), "{}", low(&s));
929 s.constrain(Constraint::Fix {
930 point: anchor,
931 x: n(4.0),
932 y: n(-3.0),
933 });
934 assert!(s.solve().unwrap().converged);
935 assert!(close(low(&s), -3.0), "{}", low(&s));
936 let far = s.add_point(n(9.0), n(9.0));
937 let line = s.add_line(anchor, far);
938 let parallel = Constraint::Parallel { a: line, b: text };
939 assert!(s.validate_constraint(¶llel).is_err());
940 }
941}