Another project
0

Configure Feed

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

1use bone_document::{ 2 ArcData, CircleData, DimensionValue, LineData, Sketch, SketchDimension, SketchEntity, 3 SketchRelation, 4}; 5use bone_kernel::{Aabb2, arc_bounding_box}; 6use bone_types::{ 7 Angle, Length, Point2, SketchDimensionId, SketchEntityId, SketchRelationId, Vec2, 8}; 9use core::f64::consts::{FRAC_1_SQRT_2, TAU}; 10use uom::si::angle::{degree, radian}; 11use uom::si::length::millimeter; 12 13use crate::pick::{EntityKindTag, PickId, PickIdError, PickIndex}; 14 15#[derive(Copy, Clone, Debug, PartialEq)] 16pub struct ScenePoint { 17 at: Point2, 18 entity: SketchEntityId, 19 pick: PickId, 20} 21 22impl ScenePoint { 23 #[must_use] 24 pub const fn at(self) -> Point2 { 25 self.at 26 } 27 28 #[must_use] 29 pub const fn pick(self) -> PickId { 30 self.pick 31 } 32} 33 34#[derive(Copy, Clone, Debug, PartialEq)] 35pub struct SceneLine { 36 a: Point2, 37 b: Point2, 38 entity: SketchEntityId, 39 pick: PickId, 40 for_construction: bool, 41} 42 43impl SceneLine { 44 #[must_use] 45 pub const fn a(self) -> Point2 { 46 self.a 47 } 48 49 #[must_use] 50 pub const fn b(self) -> Point2 { 51 self.b 52 } 53 54 #[must_use] 55 pub const fn pick(self) -> PickId { 56 self.pick 57 } 58 59 #[must_use] 60 pub const fn for_construction(self) -> bool { 61 self.for_construction 62 } 63} 64 65#[derive(Copy, Clone, Debug, PartialEq)] 66pub struct SceneArc { 67 center: Point2, 68 radius: Length, 69 start_angle: Angle, 70 sweep_angle: Angle, 71 entity: SketchEntityId, 72 pick: PickId, 73 for_construction: bool, 74} 75 76impl SceneArc { 77 #[must_use] 78 pub const fn center(self) -> Point2 { 79 self.center 80 } 81 82 #[must_use] 83 pub const fn radius(self) -> Length { 84 self.radius 85 } 86 87 #[must_use] 88 pub const fn start_angle(self) -> Angle { 89 self.start_angle 90 } 91 92 #[must_use] 93 pub const fn sweep_angle(self) -> Angle { 94 self.sweep_angle 95 } 96 97 #[must_use] 98 pub const fn pick(self) -> PickId { 99 self.pick 100 } 101 102 #[must_use] 103 pub const fn for_construction(self) -> bool { 104 self.for_construction 105 } 106} 107 108#[derive(Copy, Clone, Debug, PartialEq)] 109pub struct SceneCircle { 110 center: Point2, 111 radius: Length, 112 entity: SketchEntityId, 113 pick: PickId, 114 for_construction: bool, 115} 116 117impl SceneCircle { 118 #[must_use] 119 pub const fn center(self) -> Point2 { 120 self.center 121 } 122 123 #[must_use] 124 pub const fn radius(self) -> Length { 125 self.radius 126 } 127 128 #[must_use] 129 pub const fn pick(self) -> PickId { 130 self.pick 131 } 132 133 #[must_use] 134 pub const fn for_construction(self) -> bool { 135 self.for_construction 136 } 137} 138 139#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 140#[repr(u32)] 141pub enum RelationGlyphKind { 142 Coincident = 0, 143 Horizontal = 1, 144 Vertical = 2, 145 Parallel = 3, 146 Perpendicular = 4, 147 Tangent = 5, 148 Equal = 6, 149 Concentric = 7, 150 Fix = 8, 151 Midpoint = 9, 152} 153 154impl RelationGlyphKind { 155 #[must_use] 156 pub const fn tile_index(self) -> u32 { 157 self as u32 158 } 159 160 #[must_use] 161 pub const fn from_index(idx: u32) -> Option<Self> { 162 match idx { 163 0 => Some(Self::Coincident), 164 1 => Some(Self::Horizontal), 165 2 => Some(Self::Vertical), 166 3 => Some(Self::Parallel), 167 4 => Some(Self::Perpendicular), 168 5 => Some(Self::Tangent), 169 6 => Some(Self::Equal), 170 7 => Some(Self::Concentric), 171 8 => Some(Self::Fix), 172 9 => Some(Self::Midpoint), 173 _ => None, 174 } 175 } 176 177 #[must_use] 178 pub const fn from_relation(rel: SketchRelation) -> Self { 179 match rel { 180 SketchRelation::Coincident(_, _) => Self::Coincident, 181 SketchRelation::Horizontal(_) => Self::Horizontal, 182 SketchRelation::Vertical(_) => Self::Vertical, 183 SketchRelation::Parallel(_, _) => Self::Parallel, 184 SketchRelation::Perpendicular(_, _) => Self::Perpendicular, 185 SketchRelation::Tangent(_, _) => Self::Tangent, 186 SketchRelation::Equal(_, _) => Self::Equal, 187 SketchRelation::Concentric(_, _) => Self::Concentric, 188 SketchRelation::Midpoint { .. } => Self::Midpoint, 189 SketchRelation::Fix(_) => Self::Fix, 190 } 191 } 192 193 #[must_use] 194 pub const fn all() -> [Self; 10] { 195 [ 196 Self::Coincident, 197 Self::Horizontal, 198 Self::Vertical, 199 Self::Parallel, 200 Self::Perpendicular, 201 Self::Tangent, 202 Self::Equal, 203 Self::Concentric, 204 Self::Fix, 205 Self::Midpoint, 206 ] 207 } 208} 209 210#[derive(Copy, Clone, Debug, PartialEq)] 211pub struct SceneRelationGlyph { 212 anchor_mm: Point2, 213 offset_dir: Vec2, 214 kind: RelationGlyphKind, 215 relation: SketchRelationId, 216 pick: PickId, 217} 218 219impl SceneRelationGlyph { 220 #[must_use] 221 pub const fn anchor_mm(self) -> Point2 { 222 self.anchor_mm 223 } 224 225 #[must_use] 226 pub const fn offset_dir(self) -> Vec2 { 227 self.offset_dir 228 } 229 230 #[must_use] 231 pub const fn kind(self) -> RelationGlyphKind { 232 self.kind 233 } 234 235 #[must_use] 236 pub const fn relation(self) -> SketchRelationId { 237 self.relation 238 } 239 240 #[must_use] 241 pub const fn pick(self) -> PickId { 242 self.pick 243 } 244} 245 246#[derive(Clone, Debug, PartialEq)] 247pub struct SceneDimension { 248 anchor_mm: Point2, 249 text: String, 250 dimension: SketchDimensionId, 251 pick: PickId, 252} 253 254impl SceneDimension { 255 #[must_use] 256 pub const fn anchor_mm(&self) -> Point2 { 257 self.anchor_mm 258 } 259 260 #[must_use] 261 pub fn text(&self) -> &str { 262 &self.text 263 } 264 265 #[must_use] 266 pub const fn dimension(&self) -> SketchDimensionId { 267 self.dimension 268 } 269 270 #[must_use] 271 pub const fn pick(&self) -> PickId { 272 self.pick 273 } 274} 275 276#[derive(Clone, Debug, Default, PartialEq)] 277pub struct SketchScene { 278 points: Vec<ScenePoint>, 279 lines: Vec<SceneLine>, 280 arcs: Vec<SceneArc>, 281 circles: Vec<SceneCircle>, 282 relations: Vec<SceneRelationGlyph>, 283 dimensions: Vec<SceneDimension>, 284} 285 286impl SketchScene { 287 #[must_use] 288 pub const fn empty() -> Self { 289 Self { 290 points: Vec::new(), 291 lines: Vec::new(), 292 arcs: Vec::new(), 293 circles: Vec::new(), 294 relations: Vec::new(), 295 dimensions: Vec::new(), 296 } 297 } 298 299 pub fn extract(sketch: &Sketch) -> Result<Self, PickIdError> { 300 let with_entities = sketch 301 .entity_order() 302 .iter() 303 .copied() 304 .try_fold(Self::empty(), |acc, id| acc.push_from_sketch(sketch, id))?; 305 let with_relations = sketch 306 .relation_order() 307 .iter() 308 .copied() 309 .try_fold(with_entities, |acc, id| acc.push_relation(sketch, id))?; 310 sketch 311 .dimension_order() 312 .iter() 313 .copied() 314 .try_fold(with_relations, |acc, id| acc.push_dimension(sketch, id)) 315 } 316 317 #[must_use] 318 pub fn points(&self) -> &[ScenePoint] { 319 &self.points 320 } 321 322 #[must_use] 323 pub fn lines(&self) -> &[SceneLine] { 324 &self.lines 325 } 326 327 #[must_use] 328 pub fn arcs(&self) -> &[SceneArc] { 329 &self.arcs 330 } 331 332 #[must_use] 333 pub fn circles(&self) -> &[SceneCircle] { 334 &self.circles 335 } 336 337 #[must_use] 338 pub fn relations(&self) -> &[SceneRelationGlyph] { 339 &self.relations 340 } 341 342 #[must_use] 343 pub fn dimensions(&self) -> &[SceneDimension] { 344 &self.dimensions 345 } 346 347 #[must_use] 348 pub fn is_empty(&self) -> bool { 349 self.points.is_empty() 350 && self.lines.is_empty() 351 && self.arcs.is_empty() 352 && self.circles.is_empty() 353 && self.relations.is_empty() 354 && self.dimensions.is_empty() 355 } 356 357 #[must_use] 358 pub fn aabb(&self) -> Option<Aabb2> { 359 let points = self 360 .points 361 .iter() 362 .map(|p| Aabb2::from_corners(p.at(), p.at())); 363 let lines = self.lines.iter().map(|l| Aabb2::from_corners(l.a(), l.b())); 364 let circles = self.circles.iter().map(|c| { 365 let (cx, cy) = c.center().coords_mm(); 366 let r = c.radius().get::<millimeter>(); 367 Aabb2::from_corners( 368 Point2::from_mm(cx - r, cy - r), 369 Point2::from_mm(cx + r, cy + r), 370 ) 371 }); 372 let arcs = self 373 .arcs 374 .iter() 375 .map(|a| arc_bounding_box(a.center(), a.radius(), a.start_angle(), a.sweep_angle())); 376 points 377 .chain(lines) 378 .chain(circles) 379 .chain(arcs) 380 .reduce(|a, b| a.extend_point(b.min()).extend_point(b.max())) 381 } 382 383 pub fn pick_index(&self) -> Result<PickIndex, PickIdError> { 384 let entities = self 385 .points 386 .iter() 387 .map(|p| (p.entity, EntityKindTag::Point)) 388 .chain(self.lines.iter().map(|l| (l.entity, EntityKindTag::Line))) 389 .chain(self.arcs.iter().map(|a| (a.entity, EntityKindTag::Arc))) 390 .chain( 391 self.circles 392 .iter() 393 .map(|c| (c.entity, EntityKindTag::Circle)), 394 ); 395 PickIndex::build( 396 entities, 397 self.relations.iter().map(|g| g.relation), 398 self.dimensions.iter().map(|d| d.dimension), 399 ) 400 } 401 402 fn push_relation(mut self, sketch: &Sketch, id: SketchRelationId) -> Result<Self, PickIdError> { 403 let Some(rel) = sketch.relations().get(id).copied() else { 404 return Ok(self); 405 }; 406 let Some((anchor, offset_dir)) = relation_anchor(sketch, rel) else { 407 return Ok(self); 408 }; 409 self.relations.push(SceneRelationGlyph { 410 anchor_mm: anchor, 411 offset_dir, 412 kind: RelationGlyphKind::from_relation(rel), 413 relation: id, 414 pick: PickId::relation(id)?, 415 }); 416 Ok(self) 417 } 418 419 fn push_dimension( 420 mut self, 421 sketch: &Sketch, 422 id: SketchDimensionId, 423 ) -> Result<Self, PickIdError> { 424 let Some(dim) = sketch.dimensions().get(id).copied() else { 425 return Ok(self); 426 }; 427 let Some(anchor) = dimension_anchor(sketch, dim) else { 428 return Ok(self); 429 }; 430 self.dimensions.push(SceneDimension { 431 anchor_mm: anchor, 432 text: format_dimension(dim), 433 dimension: id, 434 pick: PickId::dimension(id)?, 435 }); 436 Ok(self) 437 } 438 439 fn push_from_sketch( 440 mut self, 441 sketch: &Sketch, 442 id: SketchEntityId, 443 ) -> Result<Self, PickIdError> { 444 let Some(entity) = sketch.entities().get(id).copied() else { 445 unreachable!( 446 "entity_order references id {id:?} missing from entities — Sketch invariants broken" 447 ); 448 }; 449 match entity { 450 SketchEntity::Point(p) => { 451 self.points.push(ScenePoint { 452 at: p.at(), 453 entity: id, 454 pick: PickId::point(id)?, 455 }); 456 } 457 SketchEntity::Line(l) => { 458 self.lines.push(SceneLine { 459 a: point_position(sketch, l.a()), 460 b: point_position(sketch, l.b()), 461 entity: id, 462 pick: PickId::line(id)?, 463 for_construction: l.for_construction(), 464 }); 465 } 466 SketchEntity::Arc(a) => { 467 if let Some(arc) = derive_arc(sketch, id, a, PickId::arc(id)?) { 468 self.arcs.push(arc); 469 } 470 } 471 SketchEntity::Circle(c) => { 472 self.circles.push(SceneCircle { 473 center: point_position(sketch, c.center()), 474 radius: c.radius(), 475 entity: id, 476 pick: PickId::circle(id)?, 477 for_construction: c.for_construction(), 478 }); 479 } 480 } 481 Ok(self) 482 } 483} 484 485fn point_position(sketch: &Sketch, id: SketchEntityId) -> Point2 { 486 let Some(entity) = sketch.entities().get(id) else { 487 unreachable!( 488 "SketchEntityId {id:?} missing from sketch entities — Sketch invariants broken" 489 ); 490 }; 491 let SketchEntity::Point(p) = entity else { 492 unreachable!( 493 "expected Point at {id:?}, got {:?} — Sketch::apply guarantees references resolve to Points", 494 entity.kind() 495 ); 496 }; 497 p.at() 498} 499 500fn relation_anchor(sketch: &Sketch, rel: SketchRelation) -> Option<(Point2, Vec2)> { 501 rel.references() 502 .into_iter() 503 .next() 504 .and_then(|id| entity_anchor(sketch, id)) 505} 506 507fn entity_anchor(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Vec2)> { 508 let e = sketch.entities().get(id)?; 509 Some(match *e { 510 SketchEntity::Point(p) => (p.at(), Vec2::from_mm(FRAC_1_SQRT_2, FRAC_1_SQRT_2)), 511 SketchEntity::Line(l) => line_anchor(sketch, l), 512 SketchEntity::Arc(a) => arc_anchor(sketch, a), 513 SketchEntity::Circle(c) => circle_anchor(sketch, c), 514 }) 515} 516 517fn line_anchor(sketch: &Sketch, line: LineData) -> (Point2, Vec2) { 518 let (ax, ay) = point_position(sketch, line.a()).coords_mm(); 519 let (bx, by) = point_position(sketch, line.b()).coords_mm(); 520 let mid = Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by)); 521 let tangent = Vec2::from_mm(bx - ax, by - ay); 522 (mid, unit_or(tangent.perp_ccw(), Vec2::from_mm(0.0, 1.0))) 523} 524 525fn arc_anchor(sketch: &Sketch, arc: ArcData) -> (Point2, Vec2) { 526 let center = point_position(sketch, arc.center()); 527 let start = point_position(sketch, arc.start()); 528 let (cx, cy) = center.coords_mm(); 529 let (sx, sy) = start.coords_mm(); 530 let radial = Vec2::from_mm(sx - cx, sy - cy); 531 (start, unit_or(radial, Vec2::from_mm(1.0, 0.0))) 532} 533 534fn circle_anchor(sketch: &Sketch, circle: CircleData) -> (Point2, Vec2) { 535 let (cx, cy) = point_position(sketch, circle.center()).coords_mm(); 536 let r_mm = circle.radius().get::<millimeter>(); 537 (Point2::from_mm(cx + r_mm, cy), Vec2::from_mm(1.0, 0.0)) 538} 539 540fn dimension_anchor(sketch: &Sketch, dim: SketchDimension) -> Option<Point2> { 541 match dim { 542 SketchDimension::Linear { a, b, .. } | SketchDimension::Angular { a, b, .. } => { 543 let (ax, ay) = entity_anchor(sketch, a)?.0.coords_mm(); 544 let (bx, by) = entity_anchor(sketch, b)?.0.coords_mm(); 545 Some(Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by))) 546 } 547 SketchDimension::Radius { target, .. } | SketchDimension::Diameter { target, .. } => { 548 target_center(sketch, target) 549 } 550 } 551} 552 553fn target_center(sketch: &Sketch, id: SketchEntityId) -> Option<Point2> { 554 match sketch.entities().get(id)? { 555 SketchEntity::Arc(a) => Some(point_position(sketch, a.center())), 556 SketchEntity::Circle(c) => Some(point_position(sketch, c.center())), 557 _ => None, 558 } 559} 560 561fn format_dimension(dim: SketchDimension) -> String { 562 let body = match (dim, dim.value()) { 563 (SketchDimension::Linear { .. }, DimensionValue::Length(len)) => { 564 format!("{:.2} mm", len.get::<millimeter>()) 565 } 566 (SketchDimension::Radius { .. }, DimensionValue::Length(len)) => { 567 format!("R {:.2}", len.get::<millimeter>()) 568 } 569 (SketchDimension::Diameter { .. }, DimensionValue::Length(len)) => { 570 format!("D {:.2}", len.get::<millimeter>()) 571 } 572 (SketchDimension::Angular { .. }, DimensionValue::Angle(a)) => { 573 format!("{:.1}°", a.get::<degree>()) 574 } 575 _ => unreachable!( 576 "SketchDimension::value pins Length for Linear/Radius/Diameter and Angle for Angular" 577 ), 578 }; 579 match dim.kind() { 580 bone_document::DimensionKind::Driving => body, 581 bone_document::DimensionKind::Driven => format!("({body})"), 582 } 583} 584 585fn unit_or(v: Vec2, fallback: Vec2) -> Vec2 { 586 let len = v.norm_mm(); 587 if len > 1e-9 { 588 let (x, y) = v.coords_mm(); 589 Vec2::from_mm(x / len, y / len) 590 } else { 591 fallback 592 } 593} 594 595fn derive_arc( 596 sketch: &Sketch, 597 id: SketchEntityId, 598 data: bone_document::ArcData, 599 pick: PickId, 600) -> Option<SceneArc> { 601 let center = point_position(sketch, data.center()); 602 let start = point_position(sketch, data.start()); 603 let end = point_position(sketch, data.end()); 604 let (cx, cy) = center.coords_mm(); 605 let (start_x, start_y) = start.coords_mm(); 606 let (end_x, end_y) = end.coords_mm(); 607 let start_offset_x = start_x - cx; 608 let start_offset_y = start_y - cy; 609 let radius_mm = (start_offset_x * start_offset_x + start_offset_y * start_offset_y).sqrt(); 610 if !(radius_mm.is_finite() && radius_mm > 0.0) { 611 return None; 612 } 613 let start_angle = start_offset_y.atan2(start_offset_x); 614 let end_angle = (end_y - cy).atan2(end_x - cx); 615 let sweep = (end_angle - start_angle).rem_euclid(TAU); 616 if !(sweep.is_finite() && sweep > 0.0) { 617 return None; 618 } 619 Some(SceneArc { 620 center, 621 radius: Length::new::<millimeter>(radius_mm), 622 start_angle: Angle::new::<radian>(start_angle), 623 sweep_angle: Angle::new::<radian>(sweep), 624 entity: id, 625 pick, 626 for_construction: data.for_construction(), 627 }) 628} 629 630#[cfg(test)] 631mod tests { 632 use super::*; 633 use bone_document::{EditOutcome, SketchEdit}; 634 use bone_types::{Point3, SketchPlaneBasis, Tolerance, UnitVec3}; 635 use uom::si::length::millimeter; 636 637 fn plane() -> SketchPlaneBasis { 638 let Ok(basis) = SketchPlaneBasis::new( 639 Point3::origin(), 640 UnitVec3::x_axis(), 641 UnitVec3::y_axis(), 642 Tolerance::new(1e-9), 643 ) else { 644 panic!("xy plane basis is orthogonal"); 645 }; 646 basis 647 } 648 649 fn len_mm(v: f64) -> Length { 650 Length::new::<millimeter>(v) 651 } 652 653 fn add_point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) { 654 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 655 SketchEntity::point(Point2::from_mm(x, y)), 656 )) else { 657 panic!("add point"); 658 }; 659 (next, id) 660 } 661 662 fn add_line( 663 sketch: Sketch, 664 a: SketchEntityId, 665 b: SketchEntityId, 666 construction: bool, 667 ) -> (Sketch, SketchEntityId) { 668 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 669 SketchEntity::line(a, b, construction), 670 )) else { 671 panic!("add line"); 672 }; 673 (next, id) 674 } 675 676 fn add_circle( 677 sketch: Sketch, 678 center: SketchEntityId, 679 radius_mm: f64, 680 ) -> (Sketch, SketchEntityId) { 681 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 682 SketchEntity::circle(center, len_mm(radius_mm), false), 683 )) else { 684 panic!("add circle"); 685 }; 686 (next, id) 687 } 688 689 fn add_arc_ids( 690 sketch: Sketch, 691 center: SketchEntityId, 692 start: SketchEntityId, 693 end: SketchEntityId, 694 ) -> (Sketch, SketchEntityId) { 695 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 696 SketchEntity::arc(center, start, end, false), 697 )) else { 698 panic!("add arc"); 699 }; 700 (next, id) 701 } 702 703 #[test] 704 fn empty_scene_has_no_items() { 705 let s = Sketch::new(plane()); 706 let Ok(scene) = SketchScene::extract(&s) else { 707 panic!("extract empty"); 708 }; 709 assert!(scene.is_empty()); 710 } 711 712 #[test] 713 fn rectangle_extracts_four_points_and_four_lines() { 714 let s = Sketch::new(plane()); 715 let (s, p0) = add_point(s, 0.0, 0.0); 716 let (s, p1) = add_point(s, 10.0, 0.0); 717 let (s, p2) = add_point(s, 10.0, 5.0); 718 let (s, p3) = add_point(s, 0.0, 5.0); 719 let (s, _) = add_line(s, p0, p1, false); 720 let (s, _) = add_line(s, p1, p2, false); 721 let (s, _) = add_line(s, p2, p3, false); 722 let (s, _) = add_line(s, p3, p0, false); 723 let Ok(scene) = SketchScene::extract(&s) else { 724 panic!("extract"); 725 }; 726 assert_eq!(scene.points().len(), 4); 727 assert_eq!(scene.lines().len(), 4); 728 assert!(scene.arcs().is_empty()); 729 assert!(scene.circles().is_empty()); 730 assert_eq!(scene.lines()[0].a(), Point2::from_mm(0.0, 0.0)); 731 assert_eq!(scene.lines()[0].b(), Point2::from_mm(10.0, 0.0)); 732 } 733 734 #[test] 735 fn construction_flag_propagates_to_line() { 736 let s = Sketch::new(plane()); 737 let (s, a) = add_point(s, 0.0, 0.0); 738 let (s, b) = add_point(s, 3.0, 4.0); 739 let (s, _) = add_line(s, a, b, true); 740 let Ok(scene) = SketchScene::extract(&s) else { 741 panic!("extract"); 742 }; 743 assert!(scene.lines()[0].for_construction()); 744 } 745 746 #[test] 747 fn circle_extracts_with_center_and_radius() { 748 let s = Sketch::new(plane()); 749 let (s, c) = add_point(s, 1.0, 2.0); 750 let (s, _) = add_circle(s, c, 4.0); 751 let Ok(scene) = SketchScene::extract(&s) else { 752 panic!("extract"); 753 }; 754 assert_eq!(scene.circles().len(), 1); 755 assert_eq!(scene.circles()[0].center(), Point2::from_mm(1.0, 2.0)); 756 assert!((scene.circles()[0].radius().get::<millimeter>() - 4.0).abs() < 1e-12); 757 } 758 759 #[test] 760 fn arc_derives_radius_and_ccw_sweep() { 761 let s = Sketch::new(plane()); 762 let (s, c) = add_point(s, 0.0, 0.0); 763 let (s, start) = add_point(s, 5.0, 0.0); 764 let (s, end) = add_point(s, 0.0, 5.0); 765 let (s, _) = add_arc_ids(s, c, start, end); 766 let Ok(scene) = SketchScene::extract(&s) else { 767 panic!("extract"); 768 }; 769 let arc = scene.arcs()[0]; 770 assert!((arc.radius().get::<millimeter>() - 5.0).abs() < 1e-12); 771 let start_rad = arc.start_angle().get::<radian>(); 772 let sweep_rad = arc.sweep_angle().get::<radian>(); 773 assert!(start_rad.abs() < 1e-12); 774 assert!((sweep_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12); 775 } 776 777 #[test] 778 fn pick_index_round_trips_each_scene_item() { 779 use crate::pick::PickedItem; 780 let s = Sketch::new(plane()); 781 let (s, p0) = add_point(s, 0.0, 0.0); 782 let (s, p1) = add_point(s, 1.0, 0.0); 783 let (s, line) = add_line(s, p0, p1, false); 784 let (s, cc) = add_point(s, 5.0, 0.0); 785 let (s, circle) = add_circle(s, cc, 2.0); 786 let (s, ac) = add_point(s, 10.0, 0.0); 787 let (s, a_start) = add_point(s, 12.0, 0.0); 788 let (s, a_end) = add_point(s, 10.0, 2.0); 789 let (s, arc) = add_arc_ids(s, ac, a_start, a_end); 790 let Ok(scene) = SketchScene::extract(&s) else { 791 panic!("extract"); 792 }; 793 let Ok(index) = scene.pick_index() else { 794 panic!("pick index"); 795 }; 796 assert_eq!( 797 scene.points()[0].pick().unpack(&index), 798 Some(PickedItem::Point(p0)) 799 ); 800 assert_eq!( 801 scene.lines()[0].pick().unpack(&index), 802 Some(PickedItem::Line(line)) 803 ); 804 assert_eq!( 805 scene.arcs()[0].pick().unpack(&index), 806 Some(PickedItem::Arc(arc)) 807 ); 808 assert_eq!( 809 scene.circles()[0].pick().unpack(&index), 810 Some(PickedItem::Circle(circle)) 811 ); 812 } 813 814 #[test] 815 fn arc_coincident_start_and_end_is_dropped() { 816 let s = Sketch::new(plane()); 817 let (s, c) = add_point(s, 0.0, 0.0); 818 let (s, start) = add_point(s, 5.0, 0.0); 819 let (s, end) = add_point(s, 5.0, 0.0); 820 let (s, _) = add_arc_ids(s, c, start, end); 821 let Ok(scene) = SketchScene::extract(&s) else { 822 panic!("extract"); 823 }; 824 assert!(scene.arcs().is_empty()); 825 } 826 827 #[test] 828 fn arc_with_zero_radius_is_dropped() { 829 let s = Sketch::new(plane()); 830 let (s, c) = add_point(s, 0.0, 0.0); 831 let (s, start) = add_point(s, 0.0, 0.0); 832 let (s, end) = add_point(s, 5.0, 0.0); 833 let (s, _) = add_arc_ids(s, c, start, end); 834 let Ok(scene) = SketchScene::extract(&s) else { 835 panic!("extract"); 836 }; 837 assert!(scene.arcs().is_empty()); 838 } 839 840 #[test] 841 fn arc_sweep_follows_ccw_convention() { 842 let s = Sketch::new(plane()); 843 let (s, c) = add_point(s, 0.0, 0.0); 844 let (s, start) = add_point(s, 0.0, 5.0); 845 let (s, end) = add_point(s, 5.0, 0.0); 846 let (s, _) = add_arc_ids(s, c, start, end); 847 let Ok(scene) = SketchScene::extract(&s) else { 848 panic!("extract"); 849 }; 850 let arc = scene.arcs()[0]; 851 let start_rad = arc.start_angle().get::<radian>(); 852 let sweep_rad = arc.sweep_angle().get::<radian>(); 853 assert!((start_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12); 854 assert!((sweep_rad - 3.0 * core::f64::consts::FRAC_PI_2).abs() < 1e-12); 855 } 856}