Another project
1use bone_document::{Sketch, SketchEntity, SketchEntityKind, SketchRelation};
2use bone_types::{IconId, SketchEntityId};
3use bone_ui::strings::StringKey;
4
5use crate::strings;
6
7#[derive(Copy, Clone, Debug, PartialEq, Eq)]
8pub enum RelationKind {
9 Coincident,
10 Horizontal,
11 Vertical,
12 Parallel,
13 Perpendicular,
14 Tangent,
15 Equal,
16 Concentric,
17 Midpoint,
18 Symmetric,
19 Fix,
20}
21
22#[derive(Clone, Debug, PartialEq)]
23pub enum Eligibility {
24 Eligible(SketchRelation),
25 Disabled(StringKey),
26}
27
28#[derive(Copy, Clone)]
29struct RelationDescriptor {
30 key: &'static str,
31 label: StringKey,
32 icon: IconId,
33 check: fn(&Sketch, &[SketchEntityId]) -> Eligibility,
34}
35
36impl RelationKind {
37 pub const ALL: &'static [Self] = &[
38 Self::Coincident,
39 Self::Horizontal,
40 Self::Vertical,
41 Self::Parallel,
42 Self::Perpendicular,
43 Self::Tangent,
44 Self::Equal,
45 Self::Concentric,
46 Self::Midpoint,
47 Self::Symmetric,
48 Self::Fix,
49 ];
50
51 const fn descriptor(self) -> RelationDescriptor {
52 match self {
53 Self::Coincident => RelationDescriptor {
54 key: "rel.coincident",
55 label: strings::TOOL_COINCIDENT,
56 icon: IconId::Coincident,
57 check: coincident,
58 },
59 Self::Horizontal => RelationDescriptor {
60 key: "rel.horizontal",
61 label: strings::TOOL_HORIZONTAL,
62 icon: IconId::Horizontal,
63 check: horizontal,
64 },
65 Self::Vertical => RelationDescriptor {
66 key: "rel.vertical",
67 label: strings::TOOL_VERTICAL,
68 icon: IconId::Vertical,
69 check: vertical,
70 },
71 Self::Parallel => RelationDescriptor {
72 key: "rel.parallel",
73 label: strings::TOOL_PARALLEL,
74 icon: IconId::Parallel,
75 check: parallel,
76 },
77 Self::Perpendicular => RelationDescriptor {
78 key: "rel.perpendicular",
79 label: strings::TOOL_PERPENDICULAR,
80 icon: IconId::Perpendicular,
81 check: perpendicular,
82 },
83 Self::Tangent => RelationDescriptor {
84 key: "rel.tangent",
85 label: strings::TOOL_TANGENT,
86 icon: IconId::Tangent,
87 check: tangent,
88 },
89 Self::Equal => RelationDescriptor {
90 key: "rel.equal",
91 label: strings::TOOL_EQUAL,
92 icon: IconId::Equal,
93 check: equal,
94 },
95 Self::Concentric => RelationDescriptor {
96 key: "rel.concentric",
97 label: strings::TOOL_CONCENTRIC,
98 icon: IconId::Concentric,
99 check: concentric,
100 },
101 Self::Midpoint => RelationDescriptor {
102 key: "rel.midpoint",
103 label: strings::TOOL_MIDPOINT,
104 icon: IconId::Midpoint,
105 check: midpoint,
106 },
107 Self::Symmetric => RelationDescriptor {
108 key: "rel.symmetric",
109 label: strings::TOOL_SYMMETRIC,
110 icon: IconId::Symmetric,
111 check: symmetric,
112 },
113 Self::Fix => RelationDescriptor {
114 key: "rel.fix",
115 label: strings::TOOL_FIX,
116 icon: IconId::Fix,
117 check: fix,
118 },
119 }
120 }
121
122 #[must_use]
123 pub const fn key(self) -> &'static str {
124 self.descriptor().key
125 }
126
127 #[must_use]
128 pub const fn label(self) -> StringKey {
129 self.descriptor().label
130 }
131
132 #[must_use]
133 pub const fn icon(self) -> IconId {
134 self.descriptor().icon
135 }
136}
137
138#[must_use]
139pub fn eligibility(
140 kind: RelationKind,
141 sketch: &Sketch,
142 selection: &[SketchEntityId],
143) -> Eligibility {
144 (kind.descriptor().check)(sketch, selection)
145}
146
147fn coincident(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
148 pair_eligibility(
149 sketch,
150 selection,
151 strings::REL_HINT_COINCIDENT,
152 |a, b| a == SketchEntityKind::Point || b == SketchEntityKind::Point,
153 SketchRelation::Coincident,
154 )
155}
156
157fn horizontal(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
158 single_eligibility(
159 sketch,
160 selection,
161 strings::REL_HINT_ONE_LINE,
162 |k| k == SketchEntityKind::Line,
163 SketchRelation::Horizontal,
164 )
165}
166
167fn vertical(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
168 single_eligibility(
169 sketch,
170 selection,
171 strings::REL_HINT_ONE_LINE,
172 |k| k == SketchEntityKind::Line,
173 SketchRelation::Vertical,
174 )
175}
176
177fn parallel(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
178 pair_eligibility(
179 sketch,
180 selection,
181 strings::REL_HINT_TWO_LINES,
182 both_lines,
183 SketchRelation::Parallel,
184 )
185}
186
187fn perpendicular(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
188 pair_eligibility(
189 sketch,
190 selection,
191 strings::REL_HINT_TWO_LINES,
192 both_lines,
193 SketchRelation::Perpendicular,
194 )
195}
196
197fn tangent(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
198 pair_eligibility(
199 sketch,
200 selection,
201 strings::REL_HINT_TANGENT,
202 |a, b| !is_point(a) && !is_point(b) && (is_curved(a) || is_curved(b)),
203 SketchRelation::Tangent,
204 )
205}
206
207fn equal(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
208 pair_eligibility(
209 sketch,
210 selection,
211 strings::REL_HINT_EQUAL,
212 |a, b| both_lines(a, b) || (is_curved(a) && is_curved(b)),
213 SketchRelation::Equal,
214 )
215}
216
217fn concentric(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
218 pair_eligibility(
219 sketch,
220 selection,
221 strings::REL_HINT_TWO_CIRCULAR,
222 |a, b| is_curved(a) && is_curved(b),
223 SketchRelation::Concentric,
224 )
225}
226
227fn midpoint(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
228 let disabled = || Eligibility::Disabled(strings::REL_HINT_MIDPOINT);
229 let Some((a, ka, b, kb)) = pair_kinds(sketch, selection) else {
230 return disabled();
231 };
232 let (point, line) = match (ka, kb) {
233 (SketchEntityKind::Point, SketchEntityKind::Line) => (a, b),
234 (SketchEntityKind::Line, SketchEntityKind::Point) => (b, a),
235 _ => return disabled(),
236 };
237 let Some(SketchEntity::Line(host)) = sketch.entities().get(line) else {
238 return disabled();
239 };
240 if host.a() == point || host.b() == point {
241 return disabled();
242 }
243 Eligibility::Eligible(SketchRelation::Midpoint { point, line })
244}
245
246fn fix(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
247 single_eligibility(
248 sketch,
249 selection,
250 strings::REL_HINT_ENTITY,
251 |_| true,
252 SketchRelation::Fix,
253 )
254}
255
256fn symmetric(sketch: &Sketch, selection: &[SketchEntityId]) -> Eligibility {
257 let disabled = || Eligibility::Disabled(strings::REL_HINT_SYMMETRIC);
258 if selection.len() != 3 {
259 return disabled();
260 }
261 let kinds: Vec<(SketchEntityId, SketchEntityKind)> = selection
262 .iter()
263 .filter_map(|id| sketch.entities().get(*id).map(|e| (*id, e.kind())))
264 .collect();
265 if kinds.len() != 3 {
266 return disabled();
267 }
268 let points: Vec<SketchEntityId> = kinds
269 .iter()
270 .filter(|(_, k)| *k == SketchEntityKind::Point)
271 .map(|(id, _)| *id)
272 .collect();
273 let lines: Vec<SketchEntityId> = kinds
274 .iter()
275 .filter(|(_, k)| *k == SketchEntityKind::Line)
276 .map(|(id, _)| *id)
277 .collect();
278 let [a, b] = points.as_slice() else {
279 return disabled();
280 };
281 let [axis] = lines.as_slice() else {
282 return disabled();
283 };
284 Eligibility::Eligible(SketchRelation::Symmetric {
285 a: *a,
286 b: *b,
287 axis: *axis,
288 })
289}
290
291fn pair_eligibility(
292 sketch: &Sketch,
293 selection: &[SketchEntityId],
294 hint: StringKey,
295 predicate: impl Fn(SketchEntityKind, SketchEntityKind) -> bool,
296 ctor: impl Fn(SketchEntityId, SketchEntityId) -> SketchRelation,
297) -> Eligibility {
298 match pair_kinds(sketch, selection) {
299 Some((a, ka, b, kb)) if predicate(ka, kb) => Eligibility::Eligible(ctor(a, b)),
300 _ => Eligibility::Disabled(hint),
301 }
302}
303
304fn single_eligibility(
305 sketch: &Sketch,
306 selection: &[SketchEntityId],
307 hint: StringKey,
308 predicate: impl Fn(SketchEntityKind) -> bool,
309 ctor: impl Fn(SketchEntityId) -> SketchRelation,
310) -> Eligibility {
311 match selection {
312 [id] => entity_kind(sketch, *id)
313 .filter(|k| predicate(*k))
314 .map_or(Eligibility::Disabled(hint), |_| {
315 Eligibility::Eligible(ctor(*id))
316 }),
317 _ => Eligibility::Disabled(hint),
318 }
319}
320
321fn pair_kinds(
322 sketch: &Sketch,
323 selection: &[SketchEntityId],
324) -> Option<(
325 SketchEntityId,
326 SketchEntityKind,
327 SketchEntityId,
328 SketchEntityKind,
329)> {
330 let [a, b] = selection else { return None };
331 if a == b {
332 return None;
333 }
334 let ka = entity_kind(sketch, *a)?;
335 let kb = entity_kind(sketch, *b)?;
336 Some((*a, ka, *b, kb))
337}
338
339fn entity_kind(sketch: &Sketch, id: SketchEntityId) -> Option<SketchEntityKind> {
340 sketch.entities().get(id).map(SketchEntity::kind)
341}
342
343const fn is_curved(kind: SketchEntityKind) -> bool {
344 matches!(kind, SketchEntityKind::Arc | SketchEntityKind::Circle)
345}
346
347const fn is_point(kind: SketchEntityKind) -> bool {
348 matches!(kind, SketchEntityKind::Point)
349}
350
351const fn both_lines(a: SketchEntityKind, b: SketchEntityKind) -> bool {
352 matches!((a, b), (SketchEntityKind::Line, SketchEntityKind::Line))
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use crate::sketch_mode::Plane;
359 use crate::tools::{add_arc, add_circle, add_line, add_point};
360 use bone_types::{Length, Point2};
361 use uom::si::length::millimeter;
362
363 fn fresh() -> Sketch {
364 Sketch::new(Plane::Xy.basis())
365 }
366
367 #[test]
368 fn relation_kind_all_matches_descriptor_arms() {
369 assert_eq!(RelationKind::ALL.len(), 11);
370 let keys: std::collections::BTreeSet<_> =
371 RelationKind::ALL.iter().map(|k| k.key()).collect();
372 assert_eq!(keys.len(), 11, "every kind has a unique widget key");
373 }
374
375 #[test]
376 fn horizontal_eligible_on_single_line() {
377 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
378 let (s, p1) = add_point(s, Point2::from_mm(5.0, 0.0));
379 let (s, line) = add_line(s, p0, p1, false);
380 assert_eq!(
381 eligibility(RelationKind::Horizontal, &s, &[line]),
382 Eligibility::Eligible(SketchRelation::Horizontal(line)),
383 );
384 }
385
386 #[test]
387 fn horizontal_disabled_on_point_selection() {
388 let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
389 assert_eq!(
390 eligibility(RelationKind::Horizontal, &s, &[p]),
391 Eligibility::Disabled(strings::REL_HINT_ONE_LINE),
392 );
393 }
394
395 #[test]
396 fn horizontal_disabled_on_empty_selection() {
397 assert_eq!(
398 eligibility(RelationKind::Horizontal, &fresh(), &[]),
399 Eligibility::Disabled(strings::REL_HINT_ONE_LINE),
400 );
401 }
402
403 #[test]
404 fn vertical_eligible_on_single_line() {
405 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
406 let (s, p1) = add_point(s, Point2::from_mm(0.0, 5.0));
407 let (s, line) = add_line(s, p0, p1, false);
408 assert_eq!(
409 eligibility(RelationKind::Vertical, &s, &[line]),
410 Eligibility::Eligible(SketchRelation::Vertical(line)),
411 );
412 }
413
414 #[test]
415 fn fix_eligible_on_any_single_entity() {
416 let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
417 assert_eq!(
418 eligibility(RelationKind::Fix, &s, &[p]),
419 Eligibility::Eligible(SketchRelation::Fix(p)),
420 );
421 }
422
423 #[test]
424 fn fix_eligible_on_line_arc_circle() {
425 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
426 let (s, p1) = add_point(s, Point2::from_mm(5.0, 0.0));
427 let (s, p2) = add_point(s, Point2::from_mm(0.0, 5.0));
428 let (s, line) = add_line(s, p0, p1, false);
429 let (s, circle) = add_circle(s, p0, Length::new::<millimeter>(2.0), false);
430 let (s, arc) = add_arc(s, p0, p1, p2, false);
431 [line, circle, arc].into_iter().for_each(|id| {
432 assert_eq!(
433 eligibility(RelationKind::Fix, &s, &[id]),
434 Eligibility::Eligible(SketchRelation::Fix(id)),
435 );
436 });
437 }
438
439 #[test]
440 fn fix_disabled_on_empty_or_pair() {
441 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
442 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
443 assert_eq!(
444 eligibility(RelationKind::Fix, &s, &[]),
445 Eligibility::Disabled(strings::REL_HINT_ENTITY),
446 );
447 assert_eq!(
448 eligibility(RelationKind::Fix, &s, &[p0, p1]),
449 Eligibility::Disabled(strings::REL_HINT_ENTITY),
450 );
451 }
452
453 #[test]
454 fn coincident_eligible_on_two_points() {
455 let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
456 let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
457 assert_eq!(
458 eligibility(RelationKind::Coincident, &s, &[a, b]),
459 Eligibility::Eligible(SketchRelation::Coincident(a, b)),
460 );
461 }
462
463 #[test]
464 fn coincident_eligible_on_point_and_line() {
465 let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
466 let (s, b) = add_point(s, Point2::from_mm(5.0, 0.0));
467 let (s, line) = add_line(s, a, b, false);
468 let (s, q) = add_point(s, Point2::from_mm(2.5, 0.0));
469 assert_eq!(
470 eligibility(RelationKind::Coincident, &s, &[q, line]),
471 Eligibility::Eligible(SketchRelation::Coincident(q, line)),
472 );
473 }
474
475 #[test]
476 fn coincident_eligible_on_point_and_circle() {
477 let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
478 let (s, circle) = add_circle(s, c, Length::new::<millimeter>(2.0), false);
479 let (s, q) = add_point(s, Point2::from_mm(1.5, 1.5));
480 assert_eq!(
481 eligibility(RelationKind::Coincident, &s, &[q, circle]),
482 Eligibility::Eligible(SketchRelation::Coincident(q, circle)),
483 );
484 }
485
486 #[test]
487 fn coincident_disabled_on_two_lines() {
488 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
489 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
490 let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
491 let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
492 let (s, l1) = add_line(s, p0, p1, false);
493 let (s, l2) = add_line(s, p2, p3, false);
494 assert_eq!(
495 eligibility(RelationKind::Coincident, &s, &[l1, l2]),
496 Eligibility::Disabled(strings::REL_HINT_COINCIDENT),
497 );
498 }
499
500 #[test]
501 fn parallel_eligible_on_two_lines() {
502 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
503 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
504 let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
505 let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
506 let (s, l1) = add_line(s, p0, p1, false);
507 let (s, l2) = add_line(s, p2, p3, false);
508 assert_eq!(
509 eligibility(RelationKind::Parallel, &s, &[l1, l2]),
510 Eligibility::Eligible(SketchRelation::Parallel(l1, l2)),
511 );
512 }
513
514 #[test]
515 fn perpendicular_disabled_on_line_and_circle() {
516 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
517 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
518 let (s, line) = add_line(s, p0, p1, false);
519 let (s, circle) = add_circle(s, p0, Length::new::<millimeter>(2.0), false);
520 assert_eq!(
521 eligibility(RelationKind::Perpendicular, &s, &[line, circle]),
522 Eligibility::Disabled(strings::REL_HINT_TWO_LINES),
523 );
524 }
525
526 #[test]
527 fn tangent_eligible_on_line_and_circle() {
528 let (s, c) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
529 let (s, p1) = add_point(s, Point2::from_mm(5.0, 0.0));
530 let (s, p2) = add_point(s, Point2::from_mm(5.0, 3.0));
531 let (s, line) = add_line(s, p1, p2, false);
532 let (s, circle) = add_circle(s, c, Length::new::<millimeter>(2.0), false);
533 assert_eq!(
534 eligibility(RelationKind::Tangent, &s, &[line, circle]),
535 Eligibility::Eligible(SketchRelation::Tangent(line, circle)),
536 );
537 assert_eq!(
538 eligibility(RelationKind::Tangent, &s, &[circle, line]),
539 Eligibility::Eligible(SketchRelation::Tangent(circle, line)),
540 );
541 }
542
543 #[test]
544 fn tangent_eligible_on_two_circles() {
545 let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
546 let (s, c2) = add_point(s, Point2::from_mm(10.0, 0.0));
547 let (s, k1) = add_circle(s, c1, Length::new::<millimeter>(3.0), false);
548 let (s, k2) = add_circle(s, c2, Length::new::<millimeter>(3.0), false);
549 assert_eq!(
550 eligibility(RelationKind::Tangent, &s, &[k1, k2]),
551 Eligibility::Eligible(SketchRelation::Tangent(k1, k2)),
552 );
553 }
554
555 #[test]
556 fn tangent_disabled_on_two_points() {
557 let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
558 let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
559 assert_eq!(
560 eligibility(RelationKind::Tangent, &s, &[a, b]),
561 Eligibility::Disabled(strings::REL_HINT_TANGENT),
562 );
563 }
564
565 #[test]
566 fn equal_eligible_on_two_lines() {
567 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
568 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
569 let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
570 let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
571 let (s, l1) = add_line(s, p0, p1, false);
572 let (s, l2) = add_line(s, p2, p3, false);
573 assert_eq!(
574 eligibility(RelationKind::Equal, &s, &[l1, l2]),
575 Eligibility::Eligible(SketchRelation::Equal(l1, l2)),
576 );
577 }
578
579 #[test]
580 fn equal_eligible_on_two_circles() {
581 let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
582 let (s, c2) = add_point(s, Point2::from_mm(10.0, 0.0));
583 let (s, k1) = add_circle(s, c1, Length::new::<millimeter>(3.0), false);
584 let (s, k2) = add_circle(s, c2, Length::new::<millimeter>(2.0), false);
585 assert_eq!(
586 eligibility(RelationKind::Equal, &s, &[k1, k2]),
587 Eligibility::Eligible(SketchRelation::Equal(k1, k2)),
588 );
589 }
590
591 #[test]
592 fn equal_disabled_on_line_and_circle() {
593 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
594 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
595 let (s, line) = add_line(s, p0, p1, false);
596 let (s, circle) = add_circle(s, p0, Length::new::<millimeter>(2.0), false);
597 assert_eq!(
598 eligibility(RelationKind::Equal, &s, &[line, circle]),
599 Eligibility::Disabled(strings::REL_HINT_EQUAL),
600 );
601 }
602
603 #[test]
604 fn concentric_eligible_on_circle_and_arc() {
605 let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
606 let (s, ks) = add_point(s, Point2::from_mm(3.0, 0.0));
607 let (s, ke) = add_point(s, Point2::from_mm(0.0, 3.0));
608 let (s, arc) = add_arc(s, c1, ks, ke, false);
609 let (s, circle) = add_circle(s, c1, Length::new::<millimeter>(2.0), false);
610 assert_eq!(
611 eligibility(RelationKind::Concentric, &s, &[arc, circle]),
612 Eligibility::Eligible(SketchRelation::Concentric(arc, circle)),
613 );
614 }
615
616 #[test]
617 fn concentric_eligible_on_two_circles() {
618 let (s, c1) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
619 let (s, c2) = add_point(s, Point2::from_mm(10.0, 0.0));
620 let (s, k1) = add_circle(s, c1, Length::new::<millimeter>(3.0), false);
621 let (s, k2) = add_circle(s, c2, Length::new::<millimeter>(2.0), false);
622 assert_eq!(
623 eligibility(RelationKind::Concentric, &s, &[k1, k2]),
624 Eligibility::Eligible(SketchRelation::Concentric(k1, k2)),
625 );
626 }
627
628 #[test]
629 fn midpoint_eligible_in_either_order() {
630 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
631 let (s, p1) = add_point(s, Point2::from_mm(10.0, 0.0));
632 let (s, line) = add_line(s, p0, p1, false);
633 let (s, q) = add_point(s, Point2::from_mm(5.0, 0.0));
634 assert_eq!(
635 eligibility(RelationKind::Midpoint, &s, &[q, line]),
636 Eligibility::Eligible(SketchRelation::Midpoint { point: q, line }),
637 );
638 assert_eq!(
639 eligibility(RelationKind::Midpoint, &s, &[line, q]),
640 Eligibility::Eligible(SketchRelation::Midpoint { point: q, line }),
641 );
642 }
643
644 #[test]
645 fn midpoint_disabled_on_two_points() {
646 let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
647 let (s, b) = add_point(s, Point2::from_mm(1.0, 0.0));
648 assert_eq!(
649 eligibility(RelationKind::Midpoint, &s, &[a, b]),
650 Eligibility::Disabled(strings::REL_HINT_MIDPOINT),
651 );
652 }
653
654 #[test]
655 fn midpoint_disabled_when_point_is_line_endpoint() {
656 let (s, a) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
657 let (s, b) = add_point(s, Point2::from_mm(10.0, 0.0));
658 let (s, line) = add_line(s, a, b, false);
659 assert_eq!(
660 eligibility(RelationKind::Midpoint, &s, &[a, line]),
661 Eligibility::Disabled(strings::REL_HINT_MIDPOINT),
662 );
663 assert_eq!(
664 eligibility(RelationKind::Midpoint, &s, &[line, b]),
665 Eligibility::Disabled(strings::REL_HINT_MIDPOINT),
666 );
667 }
668
669 #[test]
670 fn tangent_disabled_on_two_lines() {
671 let (s, p0) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
672 let (s, p1) = add_point(s, Point2::from_mm(1.0, 0.0));
673 let (s, p2) = add_point(s, Point2::from_mm(0.0, 1.0));
674 let (s, p3) = add_point(s, Point2::from_mm(1.0, 1.0));
675 let (s, l1) = add_line(s, p0, p1, false);
676 let (s, l2) = add_line(s, p2, p3, false);
677 assert_eq!(
678 eligibility(RelationKind::Tangent, &s, &[l1, l2]),
679 Eligibility::Disabled(strings::REL_HINT_TANGENT),
680 );
681 }
682
683 #[test]
684 fn pair_relations_reject_same_id_twice() {
685 let (s, p) = add_point(fresh(), Point2::from_mm(0.0, 0.0));
686 let cases = [
687 (RelationKind::Coincident, strings::REL_HINT_COINCIDENT),
688 (RelationKind::Parallel, strings::REL_HINT_TWO_LINES),
689 (RelationKind::Perpendicular, strings::REL_HINT_TWO_LINES),
690 (RelationKind::Tangent, strings::REL_HINT_TANGENT),
691 (RelationKind::Equal, strings::REL_HINT_EQUAL),
692 (RelationKind::Concentric, strings::REL_HINT_TWO_CIRCULAR),
693 (RelationKind::Midpoint, strings::REL_HINT_MIDPOINT),
694 ];
695 cases.into_iter().for_each(|(kind, hint)| {
696 assert_eq!(
697 eligibility(kind, &s, &[p, p]),
698 Eligibility::Disabled(hint),
699 "{kind:?} must reject [a, a]",
700 );
701 });
702 }
703}