Another project
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}