Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(app): relation tool eligibility

Lewis: May this revision serve well! <lu5a@proton.me>

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