Another project
0

Configure Feed

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

at main 35 kB View raw
1use bone_document::{ 2 ArcData, CircleData, DimensionValue, LineData, Sketch, SketchDimension, SketchEntity, 3 SketchRelation, 4}; 5use bone_kernel::{Aabb2, Arc2, BrepSolid, FaceMesh, SolidMesh, arc_bounding_box}; 6use bone_types::{ 7 Angle, ChordHeightTolerance, CreaseAngle, Length, LinearRgba, Point2, Point3, 8 SketchDimensionId, SketchEntityId, SketchRelationId, Tolerance, UnitVec3, Vec2, 9}; 10use core::f64::consts::FRAC_1_SQRT_2; 11use std::collections::BTreeMap; 12use uom::si::angle::degree; 13use uom::si::length::millimeter; 14 15use crate::pick::{EntityKindTag, PickId, PickIdError, PickIndex}; 16 17const ARC_TOLERANCE: Tolerance = Tolerance::new(1.0e-9); 18 19#[derive(Copy, Clone, Debug, PartialEq)] 20pub struct ScenePoint { 21 at: Point2, 22 entity: SketchEntityId, 23 pick: PickId, 24} 25 26impl ScenePoint { 27 #[must_use] 28 pub const fn at(self) -> Point2 { 29 self.at 30 } 31 32 #[must_use] 33 pub const fn pick(self) -> PickId { 34 self.pick 35 } 36} 37 38#[derive(Copy, Clone, Debug, PartialEq)] 39pub struct SceneLine { 40 a: Point2, 41 b: Point2, 42 entity: SketchEntityId, 43 pick: PickId, 44 for_construction: bool, 45} 46 47impl SceneLine { 48 #[must_use] 49 pub const fn a(self) -> Point2 { 50 self.a 51 } 52 53 #[must_use] 54 pub const fn b(self) -> Point2 { 55 self.b 56 } 57 58 #[must_use] 59 pub const fn pick(self) -> PickId { 60 self.pick 61 } 62 63 #[must_use] 64 pub const fn for_construction(self) -> bool { 65 self.for_construction 66 } 67} 68 69#[derive(Copy, Clone, Debug, PartialEq)] 70pub struct SceneArc { 71 center: Point2, 72 radius: Length, 73 start_angle: Angle, 74 sweep_angle: Angle, 75 entity: SketchEntityId, 76 pick: PickId, 77 for_construction: bool, 78} 79 80impl SceneArc { 81 #[must_use] 82 pub const fn center(self) -> Point2 { 83 self.center 84 } 85 86 #[must_use] 87 pub const fn radius(self) -> Length { 88 self.radius 89 } 90 91 #[must_use] 92 pub const fn start_angle(self) -> Angle { 93 self.start_angle 94 } 95 96 #[must_use] 97 pub const fn sweep_angle(self) -> Angle { 98 self.sweep_angle 99 } 100 101 #[must_use] 102 pub const fn pick(self) -> PickId { 103 self.pick 104 } 105 106 #[must_use] 107 pub const fn for_construction(self) -> bool { 108 self.for_construction 109 } 110} 111 112#[derive(Copy, Clone, Debug, PartialEq)] 113pub struct SceneCircle { 114 center: Point2, 115 radius: Length, 116 entity: SketchEntityId, 117 pick: PickId, 118 for_construction: bool, 119} 120 121impl SceneCircle { 122 #[must_use] 123 pub const fn center(self) -> Point2 { 124 self.center 125 } 126 127 #[must_use] 128 pub const fn radius(self) -> Length { 129 self.radius 130 } 131 132 #[must_use] 133 pub const fn pick(self) -> PickId { 134 self.pick 135 } 136 137 #[must_use] 138 pub const fn for_construction(self) -> bool { 139 self.for_construction 140 } 141} 142 143#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] 144#[repr(u32)] 145pub enum RelationGlyphKind { 146 Coincident = 0, 147 Horizontal = 1, 148 Vertical = 2, 149 Parallel = 3, 150 Perpendicular = 4, 151 Tangent = 5, 152 Equal = 6, 153 Concentric = 7, 154 Fix = 8, 155 Midpoint = 9, 156 Symmetric = 10, 157} 158 159impl RelationGlyphKind { 160 #[must_use] 161 pub const fn tile_index(self) -> u32 { 162 self as u32 163 } 164 165 #[must_use] 166 pub const fn from_index(idx: u32) -> Option<Self> { 167 match idx { 168 0 => Some(Self::Coincident), 169 1 => Some(Self::Horizontal), 170 2 => Some(Self::Vertical), 171 3 => Some(Self::Parallel), 172 4 => Some(Self::Perpendicular), 173 5 => Some(Self::Tangent), 174 6 => Some(Self::Equal), 175 7 => Some(Self::Concentric), 176 8 => Some(Self::Fix), 177 9 => Some(Self::Midpoint), 178 10 => Some(Self::Symmetric), 179 _ => None, 180 } 181 } 182 183 #[must_use] 184 pub const fn from_relation(rel: SketchRelation) -> Self { 185 match rel { 186 SketchRelation::Coincident(_, _) => Self::Coincident, 187 SketchRelation::Horizontal(_) => Self::Horizontal, 188 SketchRelation::Vertical(_) => Self::Vertical, 189 SketchRelation::Parallel(_, _) => Self::Parallel, 190 SketchRelation::Perpendicular(_, _) => Self::Perpendicular, 191 SketchRelation::Tangent(_, _) => Self::Tangent, 192 SketchRelation::Equal(_, _) => Self::Equal, 193 SketchRelation::Concentric(_, _) => Self::Concentric, 194 SketchRelation::Midpoint { .. } => Self::Midpoint, 195 SketchRelation::Symmetric { .. } => Self::Symmetric, 196 SketchRelation::Fix(_) => Self::Fix, 197 } 198 } 199 200 #[must_use] 201 pub const fn all() -> [Self; 11] { 202 [ 203 Self::Coincident, 204 Self::Horizontal, 205 Self::Vertical, 206 Self::Parallel, 207 Self::Perpendicular, 208 Self::Tangent, 209 Self::Equal, 210 Self::Concentric, 211 Self::Fix, 212 Self::Midpoint, 213 Self::Symmetric, 214 ] 215 } 216} 217 218#[derive(Copy, Clone, Debug, PartialEq)] 219pub struct SceneRelationGlyph { 220 anchor_mm: Point2, 221 offset_dir: Vec2, 222 kind: RelationGlyphKind, 223 relation: SketchRelationId, 224 pick: PickId, 225} 226 227impl SceneRelationGlyph { 228 #[must_use] 229 pub const fn anchor_mm(self) -> Point2 { 230 self.anchor_mm 231 } 232 233 #[must_use] 234 pub const fn offset_dir(self) -> Vec2 { 235 self.offset_dir 236 } 237 238 #[must_use] 239 pub const fn kind(self) -> RelationGlyphKind { 240 self.kind 241 } 242 243 #[must_use] 244 pub const fn relation(self) -> SketchRelationId { 245 self.relation 246 } 247 248 #[must_use] 249 pub const fn pick(self) -> PickId { 250 self.pick 251 } 252} 253 254#[derive(Clone, Debug, PartialEq)] 255pub struct SceneDimension { 256 anchor_mm: Point2, 257 text: String, 258 dimension: SketchDimensionId, 259 pick: PickId, 260} 261 262impl SceneDimension { 263 #[must_use] 264 pub const fn anchor_mm(&self) -> Point2 { 265 self.anchor_mm 266 } 267 268 #[must_use] 269 pub fn text(&self) -> &str { 270 &self.text 271 } 272 273 #[must_use] 274 pub const fn dimension(&self) -> SketchDimensionId { 275 self.dimension 276 } 277 278 #[must_use] 279 pub const fn pick(&self) -> PickId { 280 self.pick 281 } 282} 283 284#[derive(Clone, Debug, Default, PartialEq)] 285pub struct SketchScene { 286 points: Vec<ScenePoint>, 287 lines: Vec<SceneLine>, 288 arcs: Vec<SceneArc>, 289 circles: Vec<SceneCircle>, 290 relations: Vec<SceneRelationGlyph>, 291 dimensions: Vec<SceneDimension>, 292} 293 294impl SketchScene { 295 #[must_use] 296 pub const fn empty() -> Self { 297 Self { 298 points: Vec::new(), 299 lines: Vec::new(), 300 arcs: Vec::new(), 301 circles: Vec::new(), 302 relations: Vec::new(), 303 dimensions: Vec::new(), 304 } 305 } 306 307 pub fn extract(sketch: &Sketch) -> Result<Self, PickIdError> { 308 let with_entities = sketch 309 .entity_order() 310 .iter() 311 .copied() 312 .try_fold(Self::empty(), |acc, id| acc.push_from_sketch(sketch, id))?; 313 let with_relations = sketch 314 .relation_order() 315 .iter() 316 .copied() 317 .try_fold(with_entities, |acc, id| acc.push_relation(sketch, id))?; 318 sketch 319 .dimension_order() 320 .iter() 321 .copied() 322 .try_fold(with_relations, |acc, id| acc.push_dimension(sketch, id)) 323 } 324 325 #[must_use] 326 pub fn points(&self) -> &[ScenePoint] { 327 &self.points 328 } 329 330 #[must_use] 331 pub fn lines(&self) -> &[SceneLine] { 332 &self.lines 333 } 334 335 #[must_use] 336 pub fn arcs(&self) -> &[SceneArc] { 337 &self.arcs 338 } 339 340 #[must_use] 341 pub fn circles(&self) -> &[SceneCircle] { 342 &self.circles 343 } 344 345 #[must_use] 346 pub fn relations(&self) -> &[SceneRelationGlyph] { 347 &self.relations 348 } 349 350 #[must_use] 351 pub fn dimensions(&self) -> &[SceneDimension] { 352 &self.dimensions 353 } 354 355 #[must_use] 356 pub fn is_empty(&self) -> bool { 357 self.points.is_empty() 358 && self.lines.is_empty() 359 && self.arcs.is_empty() 360 && self.circles.is_empty() 361 && self.relations.is_empty() 362 && self.dimensions.is_empty() 363 } 364 365 #[must_use] 366 pub fn aabb(&self) -> Option<Aabb2> { 367 let points = self 368 .points 369 .iter() 370 .map(|p| Aabb2::from_corners(p.at(), p.at())); 371 let lines = self.lines.iter().map(|l| Aabb2::from_corners(l.a(), l.b())); 372 let circles = self.circles.iter().map(|c| { 373 let (cx, cy) = c.center().coords_mm(); 374 let r = c.radius().get::<millimeter>(); 375 Aabb2::from_corners( 376 Point2::from_mm(cx - r, cy - r), 377 Point2::from_mm(cx + r, cy + r), 378 ) 379 }); 380 let arcs = self 381 .arcs 382 .iter() 383 .map(|a| arc_bounding_box(a.center(), a.radius(), a.start_angle(), a.sweep_angle())); 384 points 385 .chain(lines) 386 .chain(circles) 387 .chain(arcs) 388 .reduce(|a, b| a.extend_point(b.min()).extend_point(b.max())) 389 } 390 391 pub fn pick_index(&self) -> Result<PickIndex, PickIdError> { 392 let entities = self 393 .points 394 .iter() 395 .map(|p| (p.entity, EntityKindTag::Point)) 396 .chain(self.lines.iter().map(|l| (l.entity, EntityKindTag::Line))) 397 .chain(self.arcs.iter().map(|a| (a.entity, EntityKindTag::Arc))) 398 .chain( 399 self.circles 400 .iter() 401 .map(|c| (c.entity, EntityKindTag::Circle)), 402 ); 403 PickIndex::build( 404 entities, 405 self.relations.iter().map(|g| g.relation), 406 self.dimensions.iter().map(|d| d.dimension), 407 ) 408 } 409 410 fn push_relation(mut self, sketch: &Sketch, id: SketchRelationId) -> Result<Self, PickIdError> { 411 let Some(rel) = sketch.relations().get(id).copied() else { 412 return Ok(self); 413 }; 414 let Some((anchor, offset_dir)) = relation_anchor(sketch, rel) else { 415 return Ok(self); 416 }; 417 self.relations.push(SceneRelationGlyph { 418 anchor_mm: anchor, 419 offset_dir, 420 kind: RelationGlyphKind::from_relation(rel), 421 relation: id, 422 pick: PickId::relation(id)?, 423 }); 424 Ok(self) 425 } 426 427 fn push_dimension( 428 mut self, 429 sketch: &Sketch, 430 id: SketchDimensionId, 431 ) -> Result<Self, PickIdError> { 432 let Some(dim) = sketch.dimensions().get(id).copied() else { 433 return Ok(self); 434 }; 435 let Some(anchor) = dimension_anchor(sketch, dim) else { 436 return Ok(self); 437 }; 438 self.dimensions.push(SceneDimension { 439 anchor_mm: anchor, 440 text: format_dimension(dim), 441 dimension: id, 442 pick: PickId::dimension(id)?, 443 }); 444 Ok(self) 445 } 446 447 fn push_from_sketch( 448 mut self, 449 sketch: &Sketch, 450 id: SketchEntityId, 451 ) -> Result<Self, PickIdError> { 452 let Some(entity) = sketch.entities().get(id).copied() else { 453 unreachable!( 454 "entity_order references id {id:?} missing from entities — Sketch invariants broken" 455 ); 456 }; 457 match entity { 458 SketchEntity::Point(p) => { 459 self.points.push(ScenePoint { 460 at: p.at(), 461 entity: id, 462 pick: PickId::point(id)?, 463 }); 464 } 465 SketchEntity::Line(l) => { 466 self.lines.push(SceneLine { 467 a: point_position(sketch, l.a()), 468 b: point_position(sketch, l.b()), 469 entity: id, 470 pick: PickId::line(id)?, 471 for_construction: l.for_construction(), 472 }); 473 } 474 SketchEntity::Arc(a) => { 475 if let Some(arc) = derive_arc(sketch, id, a, PickId::arc(id)?) { 476 self.arcs.push(arc); 477 } 478 } 479 SketchEntity::Circle(c) => { 480 self.circles.push(SceneCircle { 481 center: point_position(sketch, c.center()), 482 radius: c.radius(), 483 entity: id, 484 pick: PickId::circle(id)?, 485 for_construction: c.for_construction(), 486 }); 487 } 488 } 489 Ok(self) 490 } 491} 492 493fn point_position(sketch: &Sketch, id: SketchEntityId) -> Point2 { 494 let Some(entity) = sketch.entities().get(id) else { 495 unreachable!( 496 "SketchEntityId {id:?} missing from sketch entities — Sketch invariants broken" 497 ); 498 }; 499 let SketchEntity::Point(p) = entity else { 500 unreachable!( 501 "expected Point at {id:?}, got {:?} — Sketch::apply guarantees references resolve to Points", 502 entity.kind() 503 ); 504 }; 505 p.at() 506} 507 508fn relation_anchor(sketch: &Sketch, rel: SketchRelation) -> Option<(Point2, Vec2)> { 509 rel.references() 510 .into_iter() 511 .next() 512 .and_then(|id| entity_anchor(sketch, id)) 513} 514 515fn entity_anchor(sketch: &Sketch, id: SketchEntityId) -> Option<(Point2, Vec2)> { 516 let e = sketch.entities().get(id)?; 517 Some(match *e { 518 SketchEntity::Point(p) => (p.at(), Vec2::from_mm(FRAC_1_SQRT_2, FRAC_1_SQRT_2)), 519 SketchEntity::Line(l) => line_anchor(sketch, l), 520 SketchEntity::Arc(a) => arc_anchor(sketch, a), 521 SketchEntity::Circle(c) => circle_anchor(sketch, c), 522 }) 523} 524 525fn line_anchor(sketch: &Sketch, line: LineData) -> (Point2, Vec2) { 526 let (ax, ay) = point_position(sketch, line.a()).coords_mm(); 527 let (bx, by) = point_position(sketch, line.b()).coords_mm(); 528 let mid = Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by)); 529 let tangent = Vec2::from_mm(bx - ax, by - ay); 530 (mid, unit_or(tangent.perp_ccw(), Vec2::from_mm(0.0, 1.0))) 531} 532 533fn arc_anchor(sketch: &Sketch, arc: ArcData) -> (Point2, Vec2) { 534 let center = point_position(sketch, arc.center()); 535 let start = point_position(sketch, arc.start()); 536 let (cx, cy) = center.coords_mm(); 537 let (sx, sy) = start.coords_mm(); 538 let radial = Vec2::from_mm(sx - cx, sy - cy); 539 (start, unit_or(radial, Vec2::from_mm(1.0, 0.0))) 540} 541 542fn circle_anchor(sketch: &Sketch, circle: CircleData) -> (Point2, Vec2) { 543 let (cx, cy) = point_position(sketch, circle.center()).coords_mm(); 544 let r_mm = circle.radius().get::<millimeter>(); 545 (Point2::from_mm(cx + r_mm, cy), Vec2::from_mm(1.0, 0.0)) 546} 547 548fn dimension_anchor(sketch: &Sketch, dim: SketchDimension) -> Option<Point2> { 549 match dim { 550 SketchDimension::Linear { a, b, .. } | SketchDimension::Angular { a, b, .. } => { 551 let (ax, ay) = entity_anchor(sketch, a)?.0.coords_mm(); 552 let (bx, by) = entity_anchor(sketch, b)?.0.coords_mm(); 553 Some(Point2::from_mm(0.5 * (ax + bx), 0.5 * (ay + by))) 554 } 555 SketchDimension::Radius { target, .. } | SketchDimension::Diameter { target, .. } => { 556 target_center(sketch, target) 557 } 558 } 559} 560 561fn target_center(sketch: &Sketch, id: SketchEntityId) -> Option<Point2> { 562 match sketch.entities().get(id)? { 563 SketchEntity::Arc(a) => Some(point_position(sketch, a.center())), 564 SketchEntity::Circle(c) => Some(point_position(sketch, c.center())), 565 _ => None, 566 } 567} 568 569fn format_dimension(dim: SketchDimension) -> String { 570 let body = match (dim, dim.value()) { 571 (SketchDimension::Linear { .. }, DimensionValue::Length(len)) => { 572 format!("{:.2} mm", len.get::<millimeter>()) 573 } 574 (SketchDimension::Radius { .. }, DimensionValue::Length(len)) => { 575 format!("R {:.2}", len.get::<millimeter>()) 576 } 577 (SketchDimension::Diameter { .. }, DimensionValue::Length(len)) => { 578 format!("D {:.2}", len.get::<millimeter>()) 579 } 580 (SketchDimension::Angular { .. }, DimensionValue::Angle(a)) => { 581 format!("{:.1}°", a.get::<degree>()) 582 } 583 _ => unreachable!( 584 "SketchDimension::value pins Length for Linear/Radius/Diameter and Angle for Angular" 585 ), 586 }; 587 match dim.kind() { 588 bone_document::DimensionKind::Driving => body, 589 bone_document::DimensionKind::Driven => format!("({body})"), 590 } 591} 592 593fn unit_or(v: Vec2, fallback: Vec2) -> Vec2 { 594 let len = v.norm_mm(); 595 if len > 1e-9 { 596 let (x, y) = v.coords_mm(); 597 Vec2::from_mm(x / len, y / len) 598 } else { 599 fallback 600 } 601} 602 603fn derive_arc( 604 sketch: &Sketch, 605 id: SketchEntityId, 606 data: bone_document::ArcData, 607 pick: PickId, 608) -> Option<SceneArc> { 609 let center = point_position(sketch, data.center()); 610 let start = point_position(sketch, data.start()); 611 let end = point_position(sketch, data.end()); 612 let arc = Arc2::from_center_start_end(center, start, end, ARC_TOLERANCE).ok()?; 613 Some(SceneArc { 614 center: arc.center(), 615 radius: arc.radius(), 616 start_angle: arc.start_angle(), 617 sweep_angle: arc.sweep_angle(), 618 entity: id, 619 pick, 620 for_construction: data.for_construction(), 621 }) 622} 623 624const WHITE: LinearRgba = LinearRgba::new(1.0, 1.0, 1.0, 1.0); 625 626#[derive(Clone, Debug, PartialEq)] 627pub struct SolidScene { 628 positions: Vec<Point3>, 629 normals: Vec<UnitVec3>, 630 colors: Vec<LinearRgba>, 631 pick_ids: Vec<PickId>, 632 triangles: Vec<[u32; 3]>, 633} 634 635impl SolidScene { 636 #[must_use] 637 pub fn empty() -> Self { 638 Self { 639 positions: Vec::new(), 640 normals: Vec::new(), 641 colors: Vec::new(), 642 pick_ids: Vec::new(), 643 triangles: Vec::new(), 644 } 645 } 646 647 pub fn from_mesh(mesh: &SolidMesh) -> Result<Self, PickIdError> { 648 mesh.faces() 649 .iter() 650 .try_fold(Self::empty(), |mut scene, face| { 651 let Ok(base) = u32::try_from(scene.positions.len()) else { 652 panic!("solid mesh vertex count fits a u32 index"); 653 }; 654 let pick = PickId::brep_face(face.face())?; 655 scene.positions.extend_from_slice(face.positions()); 656 scene.normals.extend_from_slice(face.normals()); 657 scene.colors.extend(face.positions().iter().map(|_| WHITE)); 658 scene.pick_ids.extend(face.positions().iter().map(|_| pick)); 659 scene 660 .triangles 661 .extend(face.triangles().iter().map(|tri| tri.map(|i| i + base))); 662 Ok(scene) 663 }) 664 } 665 666 #[must_use] 667 pub fn from_parts( 668 positions: Vec<Point3>, 669 normals: Vec<UnitVec3>, 670 colors: Vec<LinearRgba>, 671 triangles: Vec<[u32; 3]>, 672 ) -> Self { 673 assert_eq!( 674 positions.len(), 675 normals.len(), 676 "solid scene needs one normal per position", 677 ); 678 assert_eq!( 679 positions.len(), 680 colors.len(), 681 "solid scene needs one color per position", 682 ); 683 let Ok(vertex_count) = u32::try_from(positions.len()) else { 684 panic!("solid scene vertex count fits a u32 index"); 685 }; 686 assert!( 687 triangles 688 .iter() 689 .flatten() 690 .all(|&index| index < vertex_count), 691 "solid scene triangle index out of range", 692 ); 693 let pick_ids = vec![PickId::NONE; positions.len()]; 694 Self { 695 positions, 696 normals, 697 colors, 698 pick_ids, 699 triangles, 700 } 701 } 702 703 #[must_use] 704 pub fn merge(mut self, other: Self) -> Self { 705 let Ok(base) = u32::try_from(self.positions.len()) else { 706 panic!("merged solid scene vertex count fits a u32 index"); 707 }; 708 self.positions.extend(other.positions); 709 self.normals.extend(other.normals); 710 self.colors.extend(other.colors); 711 self.pick_ids.extend(other.pick_ids); 712 self.triangles 713 .extend(other.triangles.into_iter().map(|tri| tri.map(|i| i + base))); 714 self 715 } 716 717 #[must_use] 718 pub fn positions(&self) -> &[Point3] { 719 &self.positions 720 } 721 722 #[must_use] 723 pub fn normals(&self) -> &[UnitVec3] { 724 &self.normals 725 } 726 727 #[must_use] 728 pub fn colors(&self) -> &[LinearRgba] { 729 &self.colors 730 } 731 732 #[must_use] 733 pub fn pick_ids(&self) -> &[PickId] { 734 &self.pick_ids 735 } 736 737 #[must_use] 738 pub fn triangles(&self) -> &[[u32; 3]] { 739 &self.triangles 740 } 741} 742 743const NORMAL_TOLERANCE: Tolerance = Tolerance::new(1.0e-12); 744const COPLANAR_DOT: f64 = 1.0 - 1.0e-9; 745 746#[derive(Copy, Clone, Debug, PartialEq)] 747pub struct GenuineEdge { 748 a: Point3, 749 b: Point3, 750 pick: PickId, 751 crease: CreaseAngle, 752} 753 754impl GenuineEdge { 755 #[must_use] 756 pub const fn new(a: Point3, b: Point3, pick: PickId, crease: CreaseAngle) -> Self { 757 Self { a, b, pick, crease } 758 } 759 760 #[must_use] 761 pub const fn a(self) -> Point3 { 762 self.a 763 } 764 765 #[must_use] 766 pub const fn b(self) -> Point3 { 767 self.b 768 } 769 770 #[must_use] 771 pub const fn pick(self) -> PickId { 772 self.pick 773 } 774 775 #[must_use] 776 pub const fn crease(self) -> CreaseAngle { 777 self.crease 778 } 779} 780 781#[derive(Copy, Clone, Debug, PartialEq)] 782pub struct SilhouetteCandidate { 783 a: Point3, 784 b: Point3, 785 normal_a: UnitVec3, 786 normal_b: UnitVec3, 787 pick: PickId, 788} 789 790impl SilhouetteCandidate { 791 #[must_use] 792 pub const fn a(self) -> Point3 { 793 self.a 794 } 795 796 #[must_use] 797 pub const fn pick(self) -> PickId { 798 self.pick 799 } 800 801 #[must_use] 802 pub const fn b(self) -> Point3 { 803 self.b 804 } 805 806 #[must_use] 807 pub const fn normal_a(self) -> UnitVec3 { 808 self.normal_a 809 } 810 811 #[must_use] 812 pub const fn normal_b(self) -> UnitVec3 { 813 self.normal_b 814 } 815} 816 817#[derive(Clone, Debug, Default, PartialEq)] 818pub struct EdgeScene { 819 genuine: Vec<GenuineEdge>, 820 silhouettes: Vec<SilhouetteCandidate>, 821} 822 823impl EdgeScene { 824 #[must_use] 825 pub const fn empty() -> Self { 826 Self { 827 genuine: Vec::new(), 828 silhouettes: Vec::new(), 829 } 830 } 831 832 #[must_use] 833 pub fn from_genuine(genuine: Vec<GenuineEdge>) -> Self { 834 Self { 835 genuine, 836 silhouettes: Vec::new(), 837 } 838 } 839 840 pub fn from_solid( 841 solid: &BrepSolid, 842 mesh: &SolidMesh, 843 chord: ChordHeightTolerance, 844 ) -> Result<Self, PickIdError> { 845 let genuine = 846 solid 847 .edges_for_render(chord) 848 .iter() 849 .try_fold(Vec::new(), |mut acc, polyline| { 850 let pick = PickId::brep_edge(polyline.edge())?; 851 let crease = polyline.crease(); 852 acc.extend(polyline.points().windows(2).map(|pair| GenuineEdge { 853 a: pair[0], 854 b: pair[1], 855 pick, 856 crease, 857 })); 858 Ok::<_, PickIdError>(acc) 859 })?; 860 let silhouettes = mesh.faces().iter().try_fold(Vec::new(), |mut acc, slab| { 861 let pick = PickId::brep_face(slab.face())?; 862 acc.extend(slab_silhouettes(slab, pick)); 863 Ok::<_, PickIdError>(acc) 864 })?; 865 Ok(Self { 866 genuine, 867 silhouettes, 868 }) 869 } 870 871 #[must_use] 872 pub fn merge(mut self, other: Self) -> Self { 873 self.genuine.extend(other.genuine); 874 self.silhouettes.extend(other.silhouettes); 875 self 876 } 877 878 #[must_use] 879 pub fn genuine(&self) -> &[GenuineEdge] { 880 &self.genuine 881 } 882 883 #[must_use] 884 pub fn silhouettes(&self) -> &[SilhouetteCandidate] { 885 &self.silhouettes 886 } 887 888 #[must_use] 889 pub fn is_empty(&self) -> bool { 890 self.genuine.is_empty() && self.silhouettes.is_empty() 891 } 892} 893 894struct EdgeAdjacency { 895 a: Point3, 896 b: Point3, 897 normals: [Option<UnitVec3>; 2], 898 incidence: usize, 899} 900 901impl EdgeAdjacency { 902 const fn new(a: Point3, b: Point3) -> Self { 903 Self { 904 a, 905 b, 906 normals: [None, None], 907 incidence: 0, 908 } 909 } 910 911 fn record(mut self, normal: UnitVec3) -> Self { 912 if self.incidence < 2 { 913 self.normals[self.incidence] = Some(normal); 914 } 915 self.incidence += 1; 916 self 917 } 918 919 fn into_candidate(self, pick: PickId) -> Option<SilhouetteCandidate> { 920 match (self.incidence, self.normals) { 921 (2, [Some(normal_a), Some(normal_b)]) if normal_a.dot(normal_b) < COPLANAR_DOT => { 922 Some(SilhouetteCandidate { 923 a: self.a, 924 b: self.b, 925 normal_a, 926 normal_b, 927 pick, 928 }) 929 } 930 _ => None, 931 } 932 } 933} 934 935fn slab_silhouettes(slab: &FaceMesh, pick: PickId) -> Vec<SilhouetteCandidate> { 936 let positions = slab.positions(); 937 slab.triangles() 938 .iter() 939 .fold( 940 BTreeMap::<EdgeKey, EdgeAdjacency>::new(), 941 |adjacency, tri| match triangle_normal(positions, *tri) { 942 Some(normal) => { 943 triangle_edges(*tri) 944 .into_iter() 945 .fold(adjacency, |mut adjacency, (i, j)| { 946 let (a, b) = (positions[i as usize], positions[j as usize]); 947 let key = EdgeKey::new(a, b); 948 let entry = adjacency 949 .remove(&key) 950 .unwrap_or_else(|| EdgeAdjacency::new(a, b)); 951 adjacency.insert(key, entry.record(normal)); 952 adjacency 953 }) 954 } 955 None => adjacency, 956 }, 957 ) 958 .into_values() 959 .filter_map(|entry| entry.into_candidate(pick)) 960 .collect() 961} 962 963fn triangle_edges(tri: [u32; 3]) -> [(u32, u32); 3] { 964 [(tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])] 965} 966 967#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 968struct VertexKey([u64; 3]); 969 970impl VertexKey { 971 fn new(p: Point3) -> Self { 972 let (x, y, z) = p.coords_mm(); 973 Self([x.to_bits(), y.to_bits(), z.to_bits()]) 974 } 975} 976 977#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 978struct EdgeKey(VertexKey, VertexKey); 979 980impl EdgeKey { 981 fn new(a: Point3, b: Point3) -> Self { 982 let (ka, kb) = (VertexKey::new(a), VertexKey::new(b)); 983 if ka <= kb { Self(ka, kb) } else { Self(kb, ka) } 984 } 985} 986 987fn triangle_normal(positions: &[Point3], tri: [u32; 3]) -> Option<UnitVec3> { 988 let a = positions[tri[0] as usize]; 989 let b = positions[tri[1] as usize]; 990 let c = positions[tri[2] as usize]; 991 (b - a).cross(c - a).try_normalize(NORMAL_TOLERANCE).ok() 992} 993 994#[cfg(test)] 995mod tests { 996 use super::*; 997 use bone_document::{EditOutcome, SketchEdit}; 998 use bone_types::{Point3, SketchPlaneBasis, Tolerance, UnitVec3}; 999 use uom::si::angle::radian; 1000 use uom::si::length::millimeter; 1001 1002 fn plane() -> SketchPlaneBasis { 1003 let Ok(basis) = SketchPlaneBasis::new( 1004 Point3::origin(), 1005 UnitVec3::x_axis(), 1006 UnitVec3::y_axis(), 1007 Tolerance::new(1e-9), 1008 ) else { 1009 panic!("xy plane basis is orthogonal"); 1010 }; 1011 basis 1012 } 1013 1014 fn len_mm(v: f64) -> Length { 1015 Length::new::<millimeter>(v) 1016 } 1017 1018 fn add_point(sketch: Sketch, x: f64, y: f64) -> (Sketch, SketchEntityId) { 1019 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 1020 SketchEntity::point(Point2::from_mm(x, y)), 1021 )) else { 1022 panic!("add point"); 1023 }; 1024 (next, id) 1025 } 1026 1027 fn add_line( 1028 sketch: Sketch, 1029 a: SketchEntityId, 1030 b: SketchEntityId, 1031 construction: bool, 1032 ) -> (Sketch, SketchEntityId) { 1033 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 1034 SketchEntity::line(a, b, construction), 1035 )) else { 1036 panic!("add line"); 1037 }; 1038 (next, id) 1039 } 1040 1041 fn add_circle( 1042 sketch: Sketch, 1043 center: SketchEntityId, 1044 radius_mm: f64, 1045 ) -> (Sketch, SketchEntityId) { 1046 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 1047 SketchEntity::circle(center, len_mm(radius_mm), false), 1048 )) else { 1049 panic!("add circle"); 1050 }; 1051 (next, id) 1052 } 1053 1054 fn add_arc_ids( 1055 sketch: Sketch, 1056 center: SketchEntityId, 1057 start: SketchEntityId, 1058 end: SketchEntityId, 1059 ) -> (Sketch, SketchEntityId) { 1060 let Ok((next, EditOutcome::Entity(id))) = sketch.apply(SketchEdit::AddEntity( 1061 SketchEntity::arc(center, start, end, false), 1062 )) else { 1063 panic!("add arc"); 1064 }; 1065 (next, id) 1066 } 1067 1068 #[test] 1069 fn empty_scene_has_no_items() { 1070 let s = Sketch::new(plane()); 1071 let Ok(scene) = SketchScene::extract(&s) else { 1072 panic!("extract empty"); 1073 }; 1074 assert!(scene.is_empty()); 1075 } 1076 1077 #[test] 1078 fn rectangle_extracts_four_points_and_four_lines() { 1079 let s = Sketch::new(plane()); 1080 let (s, p0) = add_point(s, 0.0, 0.0); 1081 let (s, p1) = add_point(s, 10.0, 0.0); 1082 let (s, p2) = add_point(s, 10.0, 5.0); 1083 let (s, p3) = add_point(s, 0.0, 5.0); 1084 let (s, _) = add_line(s, p0, p1, false); 1085 let (s, _) = add_line(s, p1, p2, false); 1086 let (s, _) = add_line(s, p2, p3, false); 1087 let (s, _) = add_line(s, p3, p0, false); 1088 let Ok(scene) = SketchScene::extract(&s) else { 1089 panic!("extract"); 1090 }; 1091 assert_eq!(scene.points().len(), 4); 1092 assert_eq!(scene.lines().len(), 4); 1093 assert!(scene.arcs().is_empty()); 1094 assert!(scene.circles().is_empty()); 1095 assert_eq!(scene.lines()[0].a(), Point2::from_mm(0.0, 0.0)); 1096 assert_eq!(scene.lines()[0].b(), Point2::from_mm(10.0, 0.0)); 1097 } 1098 1099 #[test] 1100 fn construction_flag_propagates_to_line() { 1101 let s = Sketch::new(plane()); 1102 let (s, a) = add_point(s, 0.0, 0.0); 1103 let (s, b) = add_point(s, 3.0, 4.0); 1104 let (s, _) = add_line(s, a, b, true); 1105 let Ok(scene) = SketchScene::extract(&s) else { 1106 panic!("extract"); 1107 }; 1108 assert!(scene.lines()[0].for_construction()); 1109 } 1110 1111 #[test] 1112 fn circle_extracts_with_center_and_radius() { 1113 let s = Sketch::new(plane()); 1114 let (s, c) = add_point(s, 1.0, 2.0); 1115 let (s, _) = add_circle(s, c, 4.0); 1116 let Ok(scene) = SketchScene::extract(&s) else { 1117 panic!("extract"); 1118 }; 1119 assert_eq!(scene.circles().len(), 1); 1120 assert_eq!(scene.circles()[0].center(), Point2::from_mm(1.0, 2.0)); 1121 assert!((scene.circles()[0].radius().get::<millimeter>() - 4.0).abs() < 1e-12); 1122 } 1123 1124 #[test] 1125 fn arc_derives_radius_and_ccw_sweep() { 1126 let s = Sketch::new(plane()); 1127 let (s, c) = add_point(s, 0.0, 0.0); 1128 let (s, start) = add_point(s, 5.0, 0.0); 1129 let (s, end) = add_point(s, 0.0, 5.0); 1130 let (s, _) = add_arc_ids(s, c, start, end); 1131 let Ok(scene) = SketchScene::extract(&s) else { 1132 panic!("extract"); 1133 }; 1134 let arc = scene.arcs()[0]; 1135 assert!((arc.radius().get::<millimeter>() - 5.0).abs() < 1e-12); 1136 let start_rad = arc.start_angle().get::<radian>(); 1137 let sweep_rad = arc.sweep_angle().get::<radian>(); 1138 assert!(start_rad.abs() < 1e-12); 1139 assert!((sweep_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12); 1140 } 1141 1142 #[test] 1143 fn pick_index_round_trips_each_scene_item() { 1144 use crate::pick::PickedItem; 1145 let s = Sketch::new(plane()); 1146 let (s, p0) = add_point(s, 0.0, 0.0); 1147 let (s, p1) = add_point(s, 1.0, 0.0); 1148 let (s, line) = add_line(s, p0, p1, false); 1149 let (s, cc) = add_point(s, 5.0, 0.0); 1150 let (s, circle) = add_circle(s, cc, 2.0); 1151 let (s, ac) = add_point(s, 10.0, 0.0); 1152 let (s, a_start) = add_point(s, 12.0, 0.0); 1153 let (s, a_end) = add_point(s, 10.0, 2.0); 1154 let (s, arc) = add_arc_ids(s, ac, a_start, a_end); 1155 let Ok(scene) = SketchScene::extract(&s) else { 1156 panic!("extract"); 1157 }; 1158 let Ok(index) = scene.pick_index() else { 1159 panic!("pick index"); 1160 }; 1161 assert_eq!( 1162 scene.points()[0].pick().unpack(&index), 1163 Some(PickedItem::Point(p0)) 1164 ); 1165 assert_eq!( 1166 scene.lines()[0].pick().unpack(&index), 1167 Some(PickedItem::Line(line)) 1168 ); 1169 assert_eq!( 1170 scene.arcs()[0].pick().unpack(&index), 1171 Some(PickedItem::Arc(arc)) 1172 ); 1173 assert_eq!( 1174 scene.circles()[0].pick().unpack(&index), 1175 Some(PickedItem::Circle(circle)) 1176 ); 1177 } 1178 1179 #[test] 1180 fn arc_coincident_start_and_end_is_dropped() { 1181 let s = Sketch::new(plane()); 1182 let (s, c) = add_point(s, 0.0, 0.0); 1183 let (s, start) = add_point(s, 5.0, 0.0); 1184 let (s, end) = add_point(s, 5.0, 0.0); 1185 let (s, _) = add_arc_ids(s, c, start, end); 1186 let Ok(scene) = SketchScene::extract(&s) else { 1187 panic!("extract"); 1188 }; 1189 assert!(scene.arcs().is_empty()); 1190 } 1191 1192 #[test] 1193 fn arc_with_zero_radius_is_dropped() { 1194 let s = Sketch::new(plane()); 1195 let (s, c) = add_point(s, 0.0, 0.0); 1196 let (s, start) = add_point(s, 0.0, 0.0); 1197 let (s, end) = add_point(s, 5.0, 0.0); 1198 let (s, _) = add_arc_ids(s, c, start, end); 1199 let Ok(scene) = SketchScene::extract(&s) else { 1200 panic!("extract"); 1201 }; 1202 assert!(scene.arcs().is_empty()); 1203 } 1204 1205 #[test] 1206 fn arc_sweep_follows_ccw_convention() { 1207 let s = Sketch::new(plane()); 1208 let (s, c) = add_point(s, 0.0, 0.0); 1209 let (s, start) = add_point(s, 0.0, 5.0); 1210 let (s, end) = add_point(s, 5.0, 0.0); 1211 let (s, _) = add_arc_ids(s, c, start, end); 1212 let Ok(scene) = SketchScene::extract(&s) else { 1213 panic!("extract"); 1214 }; 1215 let arc = scene.arcs()[0]; 1216 let start_rad = arc.start_angle().get::<radian>(); 1217 let sweep_rad = arc.sweep_angle().get::<radian>(); 1218 assert!((start_rad - core::f64::consts::FRAC_PI_2).abs() < 1e-12); 1219 assert!((sweep_rad - 3.0 * core::f64::consts::FRAC_PI_2).abs() < 1e-12); 1220 } 1221}