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