1use std::collections::{BTreeMap, BTreeSet};
19
20use geop_core_math::{
21 geop_error::{GeopError, GeopResult},
22 scalars::{Scalar, as_f64},
23 union_find::UnionFind,
24 vector::Vector2,
25};
26
27use serde::{Deserialize, Serialize};
28
29macro_rules! define_ids {
30 ($($(#[$doc:meta])* $name:ident => $prefix:literal),* $(,)?) => {
31 $(
32 $(#[$doc])*
33 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize)]
34 #[serde(transparent)]
35 pub struct $name(pub u64);
36
37 impl std::fmt::Display for $name {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, concat!($prefix, "{}"), self.0)
41 }
42 }
43
44 impl std::str::FromStr for $name {
46 type Err = GeopError;
47
48 fn from_str(text: &str) -> GeopResult<Self> {
49 (text.strip_prefix($prefix))
50 .and_then(|id| id.parse().ok())
51 .map($name)
52 .ok_or_else(|| {
53 GeopError::new(format!(concat!("{:?} is no id `", $prefix, "<n>`"), text))
54 })
55 }
56 }
57
58 impl<'de> Deserialize<'de> for $name {
59 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
60 d.deserialize_any(IdVisitor).map($name)
61 }
62 }
63 )*
64 };
65}
66
67struct IdVisitor;
73
74impl serde::de::Visitor<'_> for IdVisitor {
75 type Value = u64;
76
77 fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78 f.write_str("a non-negative integer id, as a number or a string")
79 }
80
81 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<u64, E> {
82 Ok(v)
83 }
84
85 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<u64, E> {
86 u64::try_from(v).map_err(|_| E::custom(format!("id {v} is negative")))
87 }
88
89 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<u64, E> {
90 v.parse()
91 .map_err(|_| E::custom(format!("{v:?} is not an integer id")))
92 }
93}
94
95define_ids!(
96 PointId => "p",
98 CurveId => "c",
100 ConstraintId => "k",
102);
103
104pub(crate) fn is_false(b: &bool) -> bool {
105 !*b
106}
107
108pub(crate) trait Uses {
111 fn points(&self) -> Vec<PointId>;
113 fn curves(&self) -> Vec<CurveId> {
115 Vec::new()
116 }
117 fn coincident(&self) -> Option<(PointId, PointId)> {
119 None
120 }
121}
122
123pub(crate) fn remove<P, C: Uses, K: Uses>(
130 all_points: &mut BTreeMap<PointId, P>,
131 all_curves: &mut BTreeMap<CurveId, C>,
132 all_constraints: &mut BTreeMap<ConstraintId, K>,
133 points: &[PointId],
134 curves: &[CurveId],
135 constraints: &[ConstraintId],
136) -> BTreeSet<CurveId> {
137 let dead_curves: BTreeSet<CurveId> = all_curves
138 .iter()
139 .filter(|(id, c)| curves.contains(id) || c.points().iter().any(|p| points.contains(p)))
140 .map(|(&id, _)| id)
141 .collect();
142 let dead_constraints: BTreeSet<ConstraintId> = all_constraints
143 .iter()
144 .filter(|(id, c)| {
145 constraints.contains(id)
146 || c.points().iter().any(|p| points.contains(p))
147 || c.curves().iter().any(|k| dead_curves.contains(k))
148 })
149 .map(|(&id, _)| id)
150 .collect();
151 let mut used_before = BTreeSet::new();
152 let mut used_after = BTreeSet::new();
153 for (id, c) in all_curves.iter() {
154 for p in c.points() {
155 used_before.insert(p);
156 if !dead_curves.contains(id) {
157 used_after.insert(p);
158 }
159 }
160 }
161 for (id, c) in all_constraints.iter() {
162 for p in c.points() {
163 used_before.insert(p);
164 if !dead_constraints.contains(id) {
165 used_after.insert(p);
166 }
167 }
168 }
169 all_points.retain(|p, _| {
170 let only_used_before = used_before.contains(p) && !used_after.contains(p);
171 !(points.contains(p) || only_used_before)
172 });
173 all_curves.retain(|id, _| !dead_curves.contains(id));
174 all_constraints.retain(|id, _| !dead_constraints.contains(id));
175 dead_curves
176}
177
178pub(crate) fn point_classes<P, K: Uses>(
183 points: &BTreeMap<PointId, P>,
184 constraints: &BTreeMap<ConstraintId, K>,
185) -> BTreeMap<PointId, PointId> {
186 let ids: Vec<PointId> = points.keys().copied().collect();
187 let index: BTreeMap<PointId, usize> = ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
188 let mut classes = UnionFind::new(ids.len());
191 for (a, b) in constraints.values().filter_map(Uses::coincident) {
192 classes.union(index[&a], index[&b]);
193 }
194 (ids.iter().enumerate())
195 .map(|(i, &id)| (id, ids[classes.find(i)]))
196 .collect()
197}
198
199#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
200pub struct Point {
201 #[serde(with = "as_f64")]
202 pub x: Scalar,
203 #[serde(with = "as_f64")]
204 pub y: Scalar,
205 #[serde(default, skip_serializing_if = "is_false")]
208 pub fixed: bool,
209}
210
211impl Point {
212 pub fn xy(&self) -> Vector2 {
213 Vector2::from_array([self.x, self.y])
214 }
215}
216
217#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
218#[serde(tag = "type")]
219pub enum CurveKind {
220 Line {
221 start: PointId,
222 end: PointId,
223 },
224 Arc {
229 start: PointId,
230 end: PointId,
231 #[serde(with = "as_f64")]
232 sweep: Scalar,
233 },
234 Circle {
235 center: PointId,
236 #[serde(with = "as_f64")]
237 radius: Scalar,
238 },
239 Spline {
244 control_points: Vec<PointId>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 shape: Option<SplineShape>,
247 },
248 Text {
255 anchor: PointId,
256 text: String,
257 #[serde(with = "as_f64")]
258 height: Scalar,
259 #[serde(with = "as_f64")]
260 angle: Scalar,
261 },
262}
263
264#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
269pub struct SplineShape {
270 pub degree: usize,
271 #[serde(with = "as_f64::vec")]
272 pub knots: Vec<Scalar>,
273 #[serde(with = "as_f64::vec")]
274 pub weights: Vec<Scalar>,
275}
276
277#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
278pub struct Curve {
279 #[serde(flatten)]
280 pub kind: CurveKind,
281 #[serde(default)]
284 pub construction: bool,
285 #[serde(default, skip_serializing_if = "is_false")]
288 pub fixed: bool,
289}
290
291impl Uses for Curve {
292 fn points(&self) -> Vec<PointId> {
293 Curve::points(self)
294 }
295}
296
297impl Curve {
298 pub fn points(&self) -> Vec<PointId> {
300 match &self.kind {
301 CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. } => {
302 vec![*start, *end]
303 }
304 CurveKind::Circle { center, .. } => vec![*center],
305 CurveKind::Spline { control_points, .. } => control_points.clone(),
306 CurveKind::Text { anchor, .. } => vec![*anchor],
307 }
308 }
309
310 pub fn endpoints(&self) -> Option<(PointId, PointId)> {
312 match &self.kind {
313 CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. } => {
314 Some((*start, *end))
315 }
316 CurveKind::Circle { .. } | CurveKind::Text { .. } => None,
317 CurveKind::Spline { control_points, .. } => {
318 Some((control_points[0], *control_points.last()?))
319 }
320 }
321 }
322}
323
324#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
327#[serde(tag = "type")]
328pub enum Constraint {
329 Coincident {
331 a: PointId,
332 b: PointId,
333 },
334 PointOnCurve {
337 point: PointId,
338 curve: CurveId,
339 },
340 Horizontal {
341 line: CurveId,
342 },
343 Vertical {
344 line: CurveId,
345 },
346 Parallel {
347 a: CurveId,
348 b: CurveId,
349 },
350 Perpendicular {
351 a: CurveId,
352 b: CurveId,
353 },
354 Collinear {
356 a: CurveId,
357 b: CurveId,
358 },
359 Tangent {
363 a: CurveId,
364 b: CurveId,
365 },
366 Equal {
368 a: CurveId,
369 b: CurveId,
370 },
371 Concentric {
373 a: CurveId,
374 b: CurveId,
375 },
376 Center {
378 point: PointId,
379 curve: CurveId,
380 },
381 Midpoint {
383 point: PointId,
384 curve: CurveId,
385 },
386 Symmetric {
388 a: PointId,
389 b: PointId,
390 line: CurveId,
391 },
392 Fix {
394 point: PointId,
395 #[serde(with = "as_f64")]
396 x: Scalar,
397 #[serde(with = "as_f64")]
398 y: Scalar,
399 },
400 Distance {
401 a: PointId,
402 b: PointId,
403 #[serde(with = "as_f64")]
404 value: Scalar,
405 },
406 DistanceX {
408 a: PointId,
409 b: PointId,
410 #[serde(with = "as_f64")]
411 value: Scalar,
412 },
413 DistanceY {
415 a: PointId,
416 b: PointId,
417 #[serde(with = "as_f64")]
418 value: Scalar,
419 },
420 PointLineDistance {
421 point: PointId,
422 line: CurveId,
423 #[serde(with = "as_f64")]
424 value: Scalar,
425 },
426 Length {
428 curve: CurveId,
429 #[serde(with = "as_f64")]
430 value: Scalar,
431 },
432 Radius {
434 curve: CurveId,
435 #[serde(with = "as_f64")]
436 value: Scalar,
437 },
438 Diameter {
440 curve: CurveId,
441 #[serde(with = "as_f64")]
442 value: Scalar,
443 },
444 Angle {
446 a: CurveId,
447 b: CurveId,
448 #[serde(with = "as_f64")]
449 value: Scalar,
450 },
451 Moved {
456 a: PointId,
457 b: PointId,
458 by: CurveId,
459 },
460 EqualSweep {
463 a: CurveId,
464 b: CurveId,
465 },
466 Offset {
470 a: CurveId,
471 b: CurveId,
472 #[serde(with = "as_f64")]
473 value: Scalar,
474 },
475 OffsetAt {
482 a: CurveId,
483 #[serde(with = "as_f64")]
484 at: Scalar,
485 b: CurveId,
486 #[serde(with = "as_f64")]
487 on: Scalar,
488 by: CurveId,
489 #[serde(default, skip_serializing_if = "is_false")]
490 across: bool,
491 },
492}
493
494impl Uses for Constraint {
495 fn points(&self) -> Vec<PointId> {
496 Constraint::points(self)
497 }
498
499 fn curves(&self) -> Vec<CurveId> {
500 Constraint::curves(self)
501 }
502
503 fn coincident(&self) -> Option<(PointId, PointId)> {
504 match *self {
505 Constraint::Coincident { a, b } => Some((a, b)),
506 _ => None,
507 }
508 }
509}
510
511impl Constraint {
512 pub fn value(&self) -> Option<Scalar> {
515 use Constraint::*;
516 match *self {
517 Distance { value, .. }
518 | DistanceX { value, .. }
519 | DistanceY { value, .. }
520 | PointLineDistance { value, .. }
521 | Length { value, .. }
522 | Radius { value, .. }
523 | Diameter { value, .. }
524 | Offset { value, .. }
525 | Angle { value, .. } => Some(value),
526 _ => None,
527 }
528 }
529
530 pub fn set_value(&mut self, v: Scalar) {
533 use Constraint::*;
534 match self {
535 Distance { value, .. }
536 | DistanceX { value, .. }
537 | DistanceY { value, .. }
538 | PointLineDistance { value, .. }
539 | Length { value, .. }
540 | Radius { value, .. }
541 | Diameter { value, .. }
542 | Offset { value, .. }
543 | Angle { value, .. } => *value = v,
544 _ => {}
545 }
546 }
547
548 pub fn points(&self) -> Vec<PointId> {
550 use Constraint::*;
551 match *self {
552 Coincident { a, b }
553 | Distance { a, b, .. }
554 | DistanceX { a, b, .. }
555 | DistanceY { a, b, .. }
556 | Symmetric { a, b, .. }
557 | Moved { a, b, .. } => vec![a, b],
558 PointOnCurve { point, .. }
559 | Midpoint { point, .. }
560 | Center { point, .. }
561 | Fix { point, .. }
562 | PointLineDistance { point, .. } => vec![point],
563 _ => Vec::new(),
564 }
565 }
566
567 pub fn curves(&self) -> Vec<CurveId> {
569 use Constraint::*;
570 match *self {
571 PointOnCurve { curve, .. }
572 | Midpoint { curve, .. }
573 | Center { curve, .. }
574 | Length { curve, .. }
575 | Radius { curve, .. }
576 | Diameter { curve, .. } => vec![curve],
577 Horizontal { line }
578 | Vertical { line }
579 | PointLineDistance { line, .. }
580 | Symmetric { line, .. }
581 | Moved { by: line, .. } => vec![line],
582 Parallel { a, b }
583 | Perpendicular { a, b }
584 | Collinear { a, b }
585 | Tangent { a, b }
586 | Equal { a, b }
587 | Concentric { a, b }
588 | Angle { a, b, .. }
589 | EqualSweep { a, b }
590 | Offset { a, b, .. } => vec![a, b],
591 OffsetAt { a, b, by, .. } => vec![a, b, by],
592 _ => Vec::new(),
593 }
594 }
595}
596
597pub type Positions = BTreeMap<PointId, Vector2>;
600
601#[derive(Clone, Debug, PartialEq)]
607pub struct Enclosure {
608 pub points: BTreeMap<PointId, Vector2>,
609 pub params: BTreeMap<CurveId, Scalar>,
611}
612
613impl Enclosure {
614 pub fn as_drawn(sketch: &Sketch) -> Self {
616 Enclosure {
617 points: sketch.points.iter().map(|(&id, p)| (id, p.xy())).collect(),
618 params: sketch
619 .curves
620 .iter()
621 .filter_map(|(&id, c)| match c.kind {
622 CurveKind::Arc { sweep, .. } => Some((id, sweep)),
623 CurveKind::Circle { radius, .. } => Some((id, radius)),
624 _ => None,
625 })
626 .collect(),
627 }
628 }
629}
630
631#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
641pub struct Sketch {
642 pub points: BTreeMap<PointId, Point>,
643 pub curves: BTreeMap<CurveId, Curve>,
644 pub constraints: BTreeMap<ConstraintId, Constraint>,
645 pub next_id: u64,
649}
650
651impl Sketch {
652 pub fn new() -> Self {
653 Self::default()
654 }
655
656 pub fn point(&self, id: PointId) -> GeopResult<&Point> {
657 self.points
658 .get(&id)
659 .ok_or_else(|| GeopError::new(format!("sketch has no point {id}")))
660 }
661
662 pub fn curve(&self, id: CurveId) -> GeopResult<&Curve> {
663 self.curves
664 .get(&id)
665 .ok_or_else(|| GeopError::new(format!("sketch has no curve {id}")))
666 }
667
668 pub fn positions(&self) -> Positions {
670 self.points.iter().map(|(&id, p)| (id, p.xy())).collect()
671 }
672
673 fn fresh_id(&mut self) -> u64 {
674 let id = self.next_id;
675 self.next_id += 1;
676 id
677 }
678
679 pub fn add_point(&mut self, x: Scalar, y: Scalar) -> PointId {
680 let id = PointId(self.fresh_id());
681 self.points.insert(id, Point { x, y, fixed: false });
682 id
683 }
684
685 pub fn new_point_at(&mut self, at: [f64; 2]) -> PointId {
687 self.add_point(Scalar::from_f64(at[0]), Scalar::from_f64(at[1]))
688 }
689
690 pub fn add_fixed_point(&mut self, x: Scalar, y: Scalar) -> PointId {
693 let id = PointId(self.fresh_id());
694 self.points.insert(id, Point { x, y, fixed: true });
695 id
696 }
697
698 pub fn add_curve(&mut self, kind: CurveKind) -> CurveId {
700 let id = CurveId(self.fresh_id());
701 self.curves.insert(
702 id,
703 Curve {
704 kind,
705 construction: false,
706 fixed: false,
707 },
708 );
709 id
710 }
711
712 pub fn add_line(&mut self, start: PointId, end: PointId) -> CurveId {
713 self.add_curve(CurveKind::Line { start, end })
714 }
715
716 pub fn add_arc(&mut self, start: PointId, end: PointId, sweep: Scalar) -> CurveId {
719 self.add_curve(CurveKind::Arc { start, end, sweep })
720 }
721
722 pub fn add_circle(&mut self, center: PointId, radius: Scalar) -> CurveId {
723 self.add_curve(CurveKind::Circle { center, radius })
724 }
725
726 pub fn add_spline(&mut self, control_points: Vec<PointId>) -> CurveId {
727 self.add_curve(CurveKind::Spline {
728 control_points,
729 shape: None,
730 })
731 }
732
733 pub fn add_text(
736 &mut self,
737 anchor: PointId,
738 text: impl Into<String>,
739 height: Scalar,
740 angle: Scalar,
741 ) -> CurveId {
742 self.add_curve(CurveKind::Text {
743 anchor,
744 text: text.into(),
745 height,
746 angle,
747 })
748 }
749
750 pub fn set_construction(&mut self, curve: CurveId, construction: bool) {
751 if let Some(c) = self.curves.get_mut(&curve) {
752 c.construction = construction;
753 }
754 }
755
756 pub fn constrain(&mut self, constraint: Constraint) -> ConstraintId {
757 let id = ConstraintId(self.fresh_id());
758 self.constraints.insert(id, constraint);
759 id
760 }
761
762 pub fn remove(&mut self, points: &[PointId], curves: &[CurveId], constraints: &[ConstraintId]) {
765 let (all_points, all_curves) = (&mut self.points, &mut self.curves);
766 remove(
767 all_points,
768 all_curves,
769 &mut self.constraints,
770 points,
771 curves,
772 constraints,
773 );
774 }
775
776 pub fn validate(&self) -> GeopResult<()> {
779 let max_id = [
780 self.points.keys().last().map(|id| id.0),
781 self.curves.keys().last().map(|id| id.0),
782 self.constraints.keys().last().map(|id| id.0),
783 ];
784 if let Some(max_id) = max_id.into_iter().flatten().max()
785 && max_id >= self.next_id
786 {
787 return Err(GeopError::new(format!(
788 "sketch uses id {max_id}, but its next_id is only {}: new entities would reuse ids",
789 self.next_id
790 )));
791 }
792 for (&i, curve) in &self.curves {
793 for p in curve.points() {
794 let point = self
795 .point(p)
796 .map_err(|e| e.with_context(format!("curve {i}")))?;
797 if curve.fixed && !point.fixed {
798 return Err(GeopError::new(format!(
799 "curve {i} is fixed, but its point {p} is not"
800 )));
801 }
802 }
803 match &curve.kind {
804 CurveKind::Line { start, end } | CurveKind::Arc { start, end, .. }
805 if start == end =>
806 {
807 return Err(GeopError::new(format!(
808 "curve {i} starts and ends at the same point"
809 )));
810 }
811 CurveKind::Arc { sweep, .. }
812 if !sweep.is_finite()
813 || !sweep.abs().definitely_less(Scalar::TWO.mul(Scalar::PI)) =>
814 {
815 return Err(GeopError::new(format!(
816 "arc {i} has sweep {sweep:?}, which is not definitely within (-2π, 2π)"
817 )));
818 }
819 CurveKind::Spline { control_points, .. } if control_points.len() < 2 => {
820 return Err(GeopError::new(format!(
821 "spline {i} needs at least 2 control points"
822 )));
823 }
824 CurveKind::Spline {
825 control_points,
826 shape: Some(shape),
827 } => {
828 let n = control_points.len();
829 if shape.degree == 0
830 || shape.degree >= n
831 || shape.knots.len() != n + shape.degree + 1
832 || shape.weights.len() != n
833 || !shape
834 .weights
835 .iter()
836 .all(|w| w.definitely_greater(Scalar::ZERO))
837 {
838 return Err(GeopError::new(format!(
839 "spline {i} of {n} control points has an inconsistent shape: {shape:?}"
840 )));
841 }
842 }
843 CurveKind::Text { height, angle, .. }
844 if !height.is_finite()
845 || !height.definitely_greater(Scalar::ZERO)
846 || !angle.is_finite() =>
847 {
848 return Err(GeopError::new(format!(
849 "text {i} is {height:?} tall, turned by {angle:?}: a text is definitely taller than nothing, and turned by a finite angle"
850 )));
851 }
852 _ => {}
853 }
854 }
855 for (&i, c) in &self.constraints {
856 self.validate_constraint(c)
857 .map_err(|e| e.with_context(format!("constraint {i} = {c:?}")))?;
858 }
859 Ok(())
860 }
861
862 pub fn validate_constraint(&self, c: &Constraint) -> GeopResult<()> {
865 use Constraint::*;
866 for p in c.points() {
867 self.point(p)?;
868 }
869 let kind = |id: CurveId| self.curve(id).map(|c| &c.kind);
870 for curve in c.curves() {
871 if matches!(kind(curve)?, CurveKind::Text { .. }) {
872 return Err(GeopError::new(format!(
873 "curve {curve} is a text, which takes no constraint: constrain the point it is placed at"
874 )));
875 }
876 }
877 let is_line = |id| Ok::<_, GeopError>(matches!(kind(id)?, CurveKind::Line { .. }));
878 let is_round = |id| {
879 Ok::<_, GeopError>(matches!(
880 kind(id)?,
881 CurveKind::Arc { .. } | CurveKind::Circle { .. }
882 ))
883 };
884 let need = |ok: bool, what: &str| {
885 if ok {
886 Ok(())
887 } else {
888 Err(GeopError::new(format!("operands must be {what}")))
889 }
890 };
891 match c {
892 Coincident { .. }
893 | Distance { .. }
894 | DistanceX { .. }
895 | DistanceY { .. }
896 | Fix { .. } => Ok(()),
897 PointOnCurve { curve, .. } => need(
898 !matches!(kind(*curve)?, CurveKind::Spline { .. }),
899 "a point and a line, arc or circle",
900 ),
901 Horizontal { line } | Vertical { line } => need(is_line(*line)?, "a line"),
902 Parallel { a, b }
903 | Perpendicular { a, b }
904 | Collinear { a, b }
905 | Angle { a, b, .. } => need(is_line(*a)? && is_line(*b)?, "two lines"),
906 Tangent { a, b } => {
907 let shared = self.shared_endpoint(*a, *b)?.is_some();
908 let two_lines = is_line(*a)? && is_line(*b)?;
909 let a_circle = matches!(kind(*a)?, CurveKind::Circle { .. });
910 let b_circle = matches!(kind(*b)?, CurveKind::Circle { .. });
911 let ok = shared && !(two_lines || a_circle || b_circle)
912 || !shared
913 && (is_round(*a)? && (is_round(*b)? || is_line(*b)?)
914 || is_line(*a)? && is_round(*b)?);
915 need(
916 ok,
917 "curves sharing an endpoint (not two lines), a line and a circle/arc, or two circles/arcs",
918 )
919 }
920 Equal { a, b } | Offset { a, b, .. } => need(
921 is_line(*a)? && is_line(*b)? || is_round(*a)? && is_round(*b)?,
922 "two lines or two circles/arcs",
923 ),
924 Concentric { a, b } => need(is_round(*a)? && is_round(*b)?, "two circles/arcs"),
925 Center { curve, .. } => need(is_round(*curve)?, "a point and a circle or arc"),
926 Midpoint { curve, .. } => need(
927 matches!(
928 kind(*curve)?,
929 CurveKind::Line { .. } | CurveKind::Arc { .. }
930 ),
931 "a point and a line or arc",
932 ),
933 Symmetric { line, .. } => need(is_line(*line)?, "two points and a line"),
934 PointLineDistance { line, .. } => need(is_line(*line)?, "a point and a line"),
935 Length { curve, .. } => need(
936 matches!(
937 kind(*curve)?,
938 CurveKind::Line { .. } | CurveKind::Arc { .. }
939 ),
940 "a line or arc",
941 ),
942 Radius { curve, .. } | Diameter { curve, .. } => {
943 need(is_round(*curve)?, "a circle or arc")
944 }
945 Moved { by, .. } => need(
946 matches!(kind(*by)?, CurveKind::Line { .. } | CurveKind::Arc { .. }),
947 "two points and a line or arc",
948 ),
949 EqualSweep { a, b } => need(
950 matches!(kind(*a)?, CurveKind::Arc { .. })
951 && matches!(kind(*b)?, CurveKind::Arc { .. }),
952 "two arcs",
953 ),
954 OffsetAt { a, b, by, .. } => need(
955 matches!(kind(*a)?, CurveKind::Spline { .. })
956 && matches!(kind(*b)?, CurveKind::Spline { .. })
957 && matches!(kind(*by)?, CurveKind::Circle { .. }),
958 "two splines and a circle",
959 ),
960 }
961 }
962
963 pub fn point_classes(&self) -> BTreeMap<PointId, PointId> {
967 point_classes(&self.points, &self.constraints)
968 }
969
970 pub fn on_line(&self, line: CurveId) -> GeopResult<BTreeSet<PointId>> {
979 let CurveKind::Line { start, end } = self.curve(line)?.kind else {
980 return Err(GeopError::new(format!("curve {line} is not a line")));
981 };
982 let class = self.point_classes();
983 let mut on = BTreeSet::new();
984 let mut mark = |p: PointId| {
985 on.insert(class[&p]);
986 };
987 mark(start);
988 mark(end);
989 for c in self.constraints.values() {
990 match *c {
991 Constraint::PointOnCurve { point, curve }
992 | Constraint::Midpoint { point, curve }
993 if curve == line =>
994 {
995 mark(point)
996 }
997 Constraint::Collinear { a, b } if a == line || b == line => {
998 let other = if a == line { b } else { a };
999 for p in self.curve(other)?.points() {
1000 mark(p);
1001 }
1002 }
1003 _ => {}
1004 }
1005 }
1006 Ok(class
1007 .iter()
1008 .filter(|(_, rep)| on.contains(rep))
1009 .map(|(&p, _)| p)
1010 .collect())
1011 }
1012
1013 pub fn tangent(&self, a: CurveId, b: CurveId) -> bool {
1015 self.constraints.values().any(|c| {
1016 matches!(*c, Constraint::Tangent { a: x, b: y } if (x, y) == (a, b) || (x, y) == (b, a))
1017 })
1018 }
1019
1020 pub fn shared_endpoint(&self, a: CurveId, b: CurveId) -> GeopResult<Option<(bool, bool)>> {
1023 let (Some((a0, a1)), Some((b0, b1))) =
1024 (self.curve(a)?.endpoints(), self.curve(b)?.endpoints())
1025 else {
1026 return Ok(None);
1027 };
1028 let class = self.point_classes();
1029 let same = |p: PointId, q: PointId| class[&p] == class[&q];
1030 Ok([(false, false), (false, true), (true, false), (true, true)]
1031 .into_iter()
1032 .find(|&(ea, eb)| same(if ea { a1 } else { a0 }, if eb { b1 } else { b0 })))
1033 }
1034}