1use crate::{
8 AddDatumArgs, AddSketchArgs, Combine, Construction, EntityRef, ExtrudeArgs, Program,
9 RevolveArgs, WorldAxis,
10};
11use geop_core_sketch::{Constraint, CurveId, PointId, Sketch};
12
13fn polygon(sketch: &mut Sketch, corners: &[[f64; 2]]) -> (Vec<PointId>, Vec<CurveId>) {
16 let points: Vec<PointId> = corners
17 .iter()
18 .map(|c| sketch.add_point(c[0], c[1]))
19 .collect();
20 let lines = (0..points.len())
21 .map(|i| sketch.add_line(points[i], points[(i + 1) % points.len()]))
22 .collect();
23 (points, lines)
24}
25
26fn solved(mut sketch: Sketch) -> Sketch {
28 let report = sketch.solve().expect("example sketches are valid");
29 assert!(
30 report.converged,
31 "example sketch does not solve: {report:?}"
32 );
33 sketch
34}
35
36fn rectangle(sketch: &mut Sketch, origin: [f64; 2], width: f64, depth: f64) -> Vec<CurveId> {
39 let [x, y] = origin;
40 let (p, l) = polygon(
42 sketch,
43 &[
44 [x + 0.05, y - 0.02],
45 [x + width, y + 0.03],
46 [x + width - 0.04, y + depth],
47 [x, y + depth + 0.01],
48 ],
49 );
50 sketch.constrain(Constraint::Fix { point: p[0], x, y });
51 sketch.constrain(Constraint::Horizontal { line: l[0] });
52 sketch.constrain(Constraint::Horizontal { line: l[2] });
53 sketch.constrain(Constraint::Vertical { line: l[1] });
54 sketch.constrain(Constraint::Vertical { line: l[3] });
55 sketch.constrain(Constraint::Length {
56 curve: l[0],
57 value: width,
58 });
59 sketch.constrain(Constraint::Length {
60 curve: l[1],
61 value: depth,
62 });
63 l
64}
65
66fn circle(sketch: &mut Sketch, center: [f64; 2], radius: f64) -> CurveId {
68 let c = sketch.add_point(center[0], center[1]);
69 let circle = sketch.add_circle(c, radius * 1.1);
70 sketch.constrain(Constraint::Fix {
71 point: c,
72 x: center[0],
73 y: center[1],
74 });
75 sketch.constrain(Constraint::Radius {
76 curve: circle,
77 value: radius,
78 });
79 circle
80}
81
82pub fn box_with_drill_hole() -> Program {
87 let mut program = Program::new();
88
89 let mut outline = Sketch::new();
90 rectangle(&mut outline, [0.0, 0.0], 2.0, 2.0);
91 program.push(
92 "outline",
93 AddSketchArgs {
94 plane: EntityRef::Plane {
95 normal: WorldAxis::Z,
96 },
97 sketch: solved(outline),
98 },
99 );
100 program.push(
101 "box",
102 ExtrudeArgs {
103 sketch: "outline".into(),
104 distance: 1.0,
105 symmetric: false,
106 combine: Combine::NewBody,
107 },
108 );
109
110 let mut hole = Sketch::new();
111 circle(&mut hole, [1.0, 1.0], 0.4);
112 program.push(
113 "hole_sketch",
114 AddSketchArgs {
115 plane: EntityRef::Face {
116 name: "extrude(box,end)".into(),
117 },
118 sketch: solved(hole),
119 },
120 );
121 program.push(
122 "hole",
123 ExtrudeArgs {
124 sketch: "hole_sketch".into(),
125 distance: -0.5,
126 symmetric: false,
127 combine: Combine::Difference {
128 target: "extrude(box)".into(),
129 },
130 },
131 );
132 program
133}
134
135pub fn cross_drilled_shaft() -> Program {
141 let mut program = Program::new();
142
143 let mut section = Sketch::new();
145 let (p, l) = polygon(
146 &mut section,
147 &[
148 [0.0, 0.0],
149 [1.0, 0.0],
150 [1.0, 1.0],
151 [0.6, 1.0],
152 [0.6, 3.0],
153 [0.0, 3.0],
154 ],
155 );
156 let axis = l[5];
157 section.constrain(Constraint::Fix {
158 point: p[0],
159 x: 0.0,
160 y: 0.0,
161 });
162 section.constrain(Constraint::Vertical { line: axis });
163 for (line, horizontal) in [
164 (l[0], true),
165 (l[1], false),
166 (l[2], true),
167 (l[3], false),
168 (l[4], true),
169 ] {
170 section.constrain(if horizontal {
171 Constraint::Horizontal { line }
172 } else {
173 Constraint::Vertical { line }
174 });
175 }
176 section.constrain(Constraint::Length {
177 curve: l[0],
178 value: 1.0,
179 });
180 section.constrain(Constraint::Length {
181 curve: l[1],
182 value: 1.0,
183 });
184 section.constrain(Constraint::Length {
185 curve: l[3],
186 value: 2.0,
187 });
188 section.constrain(Constraint::Length {
189 curve: l[4],
190 value: 0.6,
191 });
192 program.push(
193 "section",
194 AddSketchArgs {
195 plane: EntityRef::Plane {
196 normal: WorldAxis::X,
197 },
198 sketch: solved(section),
199 },
200 );
201 program.push(
202 "shaft",
203 RevolveArgs {
204 sketch: "section".into(),
205 axis,
206 combine: Combine::NewBody,
207 },
208 );
209
210 let mut bore = Sketch::new();
213 circle(&mut bore, [0.0, -2.2], 0.25);
214 program.push(
215 "bore_sketch",
216 AddSketchArgs {
217 plane: EntityRef::Plane {
218 normal: WorldAxis::Y,
219 },
220 sketch: solved(bore),
221 },
222 );
223 program.push(
224 "bore",
225 ExtrudeArgs {
226 sketch: "bore_sketch".into(),
227 distance: 3.0,
228 symmetric: true,
229 combine: Combine::Difference {
230 target: "revolve(shaft)".into(),
231 },
232 },
233 );
234 program
235}
236
237pub fn two_plates() -> Program {
240 let mut program = Program::new();
241 let mut plates = Sketch::new();
242 rectangle(&mut plates, [0.0, 0.0], 1.5, 1.0);
243 rectangle(&mut plates, [2.0, 0.0], 1.0, 1.0);
244 circle(&mut plates, [0.75, 0.5], 0.3);
245 program.push(
246 "plates_sketch",
247 AddSketchArgs {
248 plane: EntityRef::Plane {
249 normal: WorldAxis::Z,
250 },
251 sketch: solved(plates),
252 },
253 );
254 program.push(
255 "plates",
256 ExtrudeArgs {
257 sketch: "plates_sketch".into(),
258 distance: 0.25,
259 symmetric: true,
260 combine: Combine::NewBody,
261 },
262 );
263 program
264}
265
266pub fn boss_on_reference_plane() -> Program {
272 let mut program = Program::new();
273 let mut outline = Sketch::new();
274 rectangle(&mut outline, [0.0, 0.0], 2.0, 2.0);
275 program.push(
276 "outline",
277 AddSketchArgs {
278 plane: EntityRef::Plane {
279 normal: WorldAxis::Z,
280 },
281 sketch: solved(outline),
282 },
283 );
284 program.push(
285 "box",
286 ExtrudeArgs {
287 sketch: "outline".into(),
288 distance: 1.0,
289 symmetric: false,
290 combine: Combine::NewBody,
291 },
292 );
293 program.push(
294 "lifted",
295 AddDatumArgs {
296 selection: vec![EntityRef::Face {
297 name: "extrude(box,end)".into(),
298 }],
299 construction: Construction::Offset { distance: 0.5 },
300 },
301 );
302 let mut boss = Sketch::new();
303 circle(&mut boss, [1.0, 1.0], 0.5);
304 program.push(
305 "boss_sketch",
306 AddSketchArgs {
307 plane: EntityRef::Datum {
308 name: "lifted".into(),
309 },
310 sketch: solved(boss),
311 },
312 );
313 program.push(
314 "boss",
315 ExtrudeArgs {
316 sketch: "boss_sketch".into(),
317 distance: -0.75,
318 symmetric: false,
319 combine: Combine::Union {
320 target: "extrude(box)".into(),
321 },
322 },
323 );
324 program
325}
326
327pub fn handle_with_hole() -> Program {
340 let mut program = Program::new();
341
342 let mut outline = Sketch::new();
343 let outer_points = [
344 (0.01007682018456503, 0.8939399106232252),
345 (-0.9693901017551558, 0.8213868052943569),
346 (-1.00566665441959, 0.3175457960661055),
347 (-0.9452057333121998, -0.3555857922628385),
348 (0.09472210973491128, -0.9481028191152622),
349 (0.695300592734987, -0.3878316168534466),
350 (1.384555093359235, -0.4523232660346628),
351 (1.6233271012047248, -0.9224957048864859),
352 (1.9214381194506664, 0.15401630544608172),
353 (1.2092840203075834, 0.8578895429712221),
354 ]
355 .map(|(x, y)| outline.add_point(x, y));
356 let mut outer_loop = outer_points.to_vec();
357 outer_loop.push(outer_points[0]);
358 outline.add_spline(outer_loop);
359
360 let inner_points = [
361 (0.27854547786607586, 0.568500810276078),
362 (-0.3241022513094386, 0.561767316095346),
363 (-0.3645032163938306, 0.00962079327532167),
364 (0.03950643445008967, -0.4246895813818926),
365 (0.507484280010964, -0.11831559615858644),
366 (0.8643594715897602, 0.12072344725739975),
367 (1.1202322504575766, -0.1452495728815144),
368 (1.2919363520662426, 0.18805838906471978),
369 (1.0427970673791584, 0.470865144655464),
370 ]
371 .map(|(x, y)| outline.add_point(x, y));
372 let mut inner_loop = inner_points.to_vec();
373 inner_loop.push(inner_points[0]);
374 outline.add_spline(inner_loop);
375
376 program.push(
377 "outline",
378 AddSketchArgs {
379 plane: EntityRef::Plane {
380 normal: WorldAxis::Z,
381 },
382 sketch: solved(outline),
383 },
384 );
385 program.push(
386 "handle",
387 ExtrudeArgs {
388 sketch: "outline".into(),
389 distance: 1.0,
390 symmetric: false,
391 combine: Combine::NewBody,
392 },
393 );
394
395 let mut hole = Sketch::new();
396 let center = hole.add_point(0.32989396295411244, -0.5632891678453777);
397 hole.add_circle(center, 0.31727744879927977);
398 program.push(
399 "hole_sketch",
400 AddSketchArgs {
401 plane: EntityRef::Plane {
402 normal: WorldAxis::Y,
403 },
404 sketch: solved(hole),
405 },
406 );
407 program.push(
408 "hole",
409 ExtrudeArgs {
410 sketch: "hole_sketch".into(),
411 distance: 2.44,
412 symmetric: true,
413 combine: Combine::Difference {
414 target: "extrude(handle)".into(),
415 },
416 },
417 );
418 program
419}
420
421pub fn luggage_tag() -> Program {
426 let mut program = Program::new();
427
428 let mut outline = Sketch::new();
429 let p0 = outline.add_point(-0.7051397478635735, 0.7809633733011653);
430 let p1 = outline.add_point(-0.7051397478646341, -0.6729821470985564);
431 let p4 = outline.add_point(0.695311597099524, -0.6729821470973941);
432 let p7 = outline.add_point(0.695311597101934, 0.7809633732995341);
433 let left = outline.add_line(p0, p1);
434 let bottom = outline.add_line(p1, p4);
435 let right = outline.add_line(p4, p7);
436 let top = outline.add_arc_with_sweep(p7, p0, std::f64::consts::PI);
437 outline.constrain(Constraint::Vertical { line: left });
438 outline.constrain(Constraint::Horizontal { line: bottom });
439 outline.constrain(Constraint::Vertical { line: right });
440 outline.constrain(Constraint::Tangent { a: top, b: right });
441 outline.constrain(Constraint::Tangent { a: top, b: left });
442
443 let hole_center = outline.add_point(-0.004914075380876311, 0.7809633733016882);
444 let hole = outline.add_circle(hole_center, 0.30711303824557856);
445 outline.constrain(Constraint::Concentric { a: hole, b: top });
446
447 let w_tl = outline.add_point(-0.5551397478558091, 0.08482972707824256);
448 let w_tr = outline.add_point(0.5453115971039223, 0.08482972707824256);
449 let w_br = outline.add_point(0.5453115971014348, -0.5229821471055477);
450 let w_bl = outline.add_point(-0.5551397478598076, -0.5229821471049918);
451 let w_top = outline.add_line(w_tl, w_tr);
452 let w_right = outline.add_line(w_tr, w_br);
453 let w_bottom = outline.add_line(w_br, w_bl);
454 let w_left = outline.add_line(w_bl, w_tl);
455 outline.constrain(Constraint::Horizontal { line: w_top });
456 outline.constrain(Constraint::Vertical { line: w_right });
457 outline.constrain(Constraint::Horizontal { line: w_bottom });
458 outline.constrain(Constraint::Vertical { line: w_left });
459 outline.constrain(Constraint::PointLineDistance {
460 point: w_br,
461 line: right,
462 value: 0.15,
463 });
464 outline.constrain(Constraint::PointLineDistance {
465 point: w_bl,
466 line: left,
467 value: 0.15,
468 });
469 outline.constrain(Constraint::PointLineDistance {
470 point: w_br,
471 line: bottom,
472 value: 0.15,
473 });
474
475 program.push(
476 "outline",
477 AddSketchArgs {
478 plane: EntityRef::Plane {
479 normal: WorldAxis::Z,
480 },
481 sketch: solved(outline),
482 },
483 );
484 program.push(
485 "tag",
486 ExtrudeArgs {
487 sketch: "outline".into(),
488 distance: 1.0 / 3.0,
489 symmetric: true,
490 combine: Combine::NewBody,
491 },
492 );
493 program
494}
495
496pub fn all() -> Vec<(&'static str, Program)> {
498 vec![
499 ("box_with_drill_hole", box_with_drill_hole()),
500 ("cross_drilled_shaft", cross_drilled_shaft()),
501 ("two_plates", two_plates()),
502 ("boss_on_reference_plane", boss_on_reference_plane()),
503 ("handle_with_hole", handle_with_hole()),
504 ("luggage_tag", luggage_tag()),
505 ]
506}
507
508#[cfg(test)]
509mod tests {
510 use std::collections::BTreeMap;
511
512 use geop_core_math::{scalars::ScalInF64 as S, scalars::Scalar, vector::Vector3};
513 use geop_core_part::{Part, PartDescription, RefId};
514 use geop_core_topology::{
515 contains::shell::{PointClassification, shell_contains},
516 validation::{ValidationParameters, validate},
517 };
518
519 use super::*;
520
521 fn outputs_dir() -> std::path::PathBuf {
522 let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../outputs/parts");
523 std::fs::create_dir_all(&dir).unwrap();
524 dir
525 }
526
527 fn geometry_by_name(part: &Part<S>) -> BTreeMap<String, String> {
531 let model = part.topology();
532 part.names()
533 .iter()
534 .filter_map(|(id, name)| {
535 let geometry = match id {
536 RefId::Vertex(v) => format!("{:?}", model.get_vertex(v).ok()?.point),
537 RefId::Edge(e) => format!("{:?}", model.get_edge(e).ok()?.curve),
538 RefId::Face(f) => format!("{:?}", model.get_face(f).ok()?.surface),
539 RefId::Solid(_) | RefId::Sketch(_) | RefId::Datum(_) => return None,
540 };
541 Some((name.to_string(), geometry))
542 })
543 .collect()
544 }
545
546 fn build_and_round_trip(name: &str, program: &Program) -> Part<S> {
552 let part = program.apply(Part::<S>::new()).unwrap();
553 let validation = ValidationParameters::default();
554 if let Err(errors) = validate(&validation, part.topology()) {
555 panic!(
556 "{name}: {} validation error(s): {}",
557 errors.len(),
558 errors[0]
559 );
560 }
561
562 let json = program.to_json().unwrap();
563 let dir = outputs_dir();
564 std::fs::write(dir.join(format!("{name}.program.json")), &json).unwrap();
565 let description = PartDescription::of(&part).unwrap();
566 std::fs::write(
567 dir.join(format!("{name}.part.json")),
568 serde_json::to_string_pretty(&description).unwrap(),
569 )
570 .unwrap();
571 geop_ops_rasterize::rasterize_model(part.topology(), 16)
572 .unwrap()
573 .save_to_file(dir.join(format!("{name}.html")).to_str().unwrap())
574 .unwrap();
575
576 let read_back = Program::from_json(&json).unwrap();
577 assert_eq!(
578 &read_back, program,
579 "{name}: JSON round trip changed the program"
580 );
581 assert_eq!(
582 read_back.to_json().unwrap(),
583 json,
584 "{name}: JSON is not stable"
585 );
586
587 let rebuilt = read_back.apply(Part::<S>::new()).unwrap();
588 assert_eq!(
589 PartDescription::of(&rebuilt).unwrap(),
590 description,
591 "{name}: the read-back program built a different part"
592 );
593 assert_eq!(
594 geometry_by_name(&rebuilt),
595 geometry_by_name(&part),
596 "{name}: the read-back program built different geometry"
597 );
598 part
599 }
600
601 fn inside(part: &Part<S>, solid: &str, p: [f64; 3]) -> PointClassification {
602 let model = part.topology();
603 let solid = part.solid_id(solid).unwrap();
604 let point = Vector3::from_array(p.map(S::from_f64));
605 let mut result = PointClassification::Outside;
606 for &shell in &model.get_solid(solid).unwrap().shells {
607 match shell_contains(model, shell, point, 2000, S::from_f64(1e-6), 7).unwrap() {
608 PointClassification::Outside => {}
609 other => result = other,
610 }
611 }
612 result
613 }
614
615 #[test]
616 fn box_with_drill_hole_round_trips() {
617 let part = build_and_round_trip("box_with_drill_hole", &box_with_drill_hole());
618 let description = PartDescription::of(&part).unwrap();
619
620 assert_eq!(
622 description.solids.keys().collect::<Vec<_>>(),
623 ["extrude(hole)"]
624 );
625 let top = &description.faces["extrude(box,end)"];
627 assert_eq!(top.holes.len(), 1, "{top:?}");
628 assert!(description.faces.contains_key("extrude(hole,end)"));
631 let circle = box_with_drill_hole().steps[2].clone();
632 let crate::PartOperation::AddSketch(args) = circle.operation else {
633 unreachable!()
634 };
635 let circle_id = *args.sketch.curves.keys().next().unwrap();
636 for piece in ["", "#1", "#2", "#3"] {
637 let wall = format!("extrude(hole,hole_sketch,{circle_id}{piece})");
638 assert!(description.faces.contains_key(&wall), "no face {wall}");
639 }
640
641 assert_eq!(
642 inside(&part, "extrude(hole)", [0.3, 0.3, 0.5]),
643 PointClassification::Inside
644 );
645 assert_eq!(
646 inside(&part, "extrude(hole)", [1.0, 1.0, 0.8]),
647 PointClassification::Outside
648 );
649 assert_eq!(
650 inside(&part, "extrude(hole)", [1.0, 1.0, 0.3]),
651 PointClassification::Inside
652 );
653 }
654
655 #[test]
656 fn cross_drilled_shaft_round_trips() {
657 let part = build_and_round_trip("cross_drilled_shaft", &cross_drilled_shaft());
658 let description = PartDescription::of(&part).unwrap();
659 assert_eq!(
660 description.solids.keys().collect::<Vec<_>>(),
661 ["extrude(bore)"]
662 );
663 assert_eq!(
664 inside(&part, "extrude(bore)", [0.0, 0.8, 0.5]),
665 PointClassification::Inside
666 );
667 assert_eq!(
668 inside(&part, "extrude(bore)", [0.0, 0.0, 2.2]),
669 PointClassification::Outside
670 );
671 assert_eq!(
672 inside(&part, "extrude(bore)", [0.0, 0.0, 2.7]),
673 PointClassification::Inside
674 );
675 }
676
677 #[test]
678 fn two_plates_round_trip() {
679 let part = build_and_round_trip("two_plates", &two_plates());
680 let description = PartDescription::of(&part).unwrap();
681 assert_eq!(
682 description.solids["extrude(plates)"].len(),
683 2,
684 "one shell per plate"
685 );
686 assert_eq!(
687 inside(&part, "extrude(plates)", [0.2, 0.2, 0.0]),
688 PointClassification::Inside
689 );
690 assert_eq!(
691 inside(&part, "extrude(plates)", [0.75, 0.5, 0.0]),
692 PointClassification::Outside
693 );
694 assert_eq!(
695 inside(&part, "extrude(plates)", [2.5, 0.5, 0.1]),
696 PointClassification::Inside
697 );
698 }
699
700 #[test]
701 fn boss_on_reference_plane_round_trips() {
702 let part = build_and_round_trip("boss_on_reference_plane", &boss_on_reference_plane());
703 let description = PartDescription::of(&part).unwrap();
704 assert_eq!(
705 description.solids.keys().collect::<Vec<_>>(),
706 ["extrude(boss)"]
707 );
708 assert_eq!(description.datums, ["lifted"]);
709 assert_eq!(
711 inside(&part, "extrude(boss)", [1.0, 1.0, 1.3]),
712 PointClassification::Inside
713 );
714 assert_eq!(
715 inside(&part, "extrude(boss)", [1.0, 1.0, 1.6]),
716 PointClassification::Outside
717 );
718 assert_eq!(
719 inside(&part, "extrude(boss)", [0.2, 0.2, 1.3]),
720 PointClassification::Outside
721 );
722 assert_eq!(
723 inside(&part, "extrude(boss)", [0.2, 0.2, 0.5]),
724 PointClassification::Inside
725 );
726 }
727
728 #[test]
731 fn names_survive_a_change_of_dimensions() {
732 let names = |program: &Program| {
733 let part = program.apply(Part::<S>::new()).unwrap();
734 let description = PartDescription::of(&part).unwrap();
735 (
736 description.faces.keys().cloned().collect::<Vec<_>>(),
737 description.edges.keys().cloned().collect::<Vec<_>>(),
738 description.vertices.keys().cloned().collect::<Vec<_>>(),
739 )
740 };
741 let original = box_with_drill_hole();
742 let mut edited = original.clone();
743 for step in &mut edited.steps {
744 match &mut step.operation {
745 crate::PartOperation::Extrude(args) if step.id == "box" => args.distance = 1.5,
746 crate::PartOperation::AddSketch(args) if step.id == "hole_sketch" => {
747 for c in args.sketch.constraints.values_mut() {
748 if let Constraint::Radius { value, .. } = c {
749 *value = 0.6;
750 }
751 }
752 args.sketch.solve().unwrap();
753 }
754 _ => {}
755 }
756 }
757 assert_ne!(original, edited);
758 assert_eq!(names(&edited), names(&original));
759 }
760
761 #[test]
763 fn referring_to_a_missing_entity_fails() {
764 let mut program = box_with_drill_hole();
765 let crate::PartOperation::AddSketch(args) = &mut program.steps[2].operation else {
766 unreachable!()
767 };
768 args.plane = EntityRef::Face {
769 name: "extrude(box,side)".into(),
770 };
771 let Err(err) = program.apply(Part::<S>::new()) else {
772 panic!("a sketch on a face that doesn't exist was placed somewhere");
773 };
774 assert!(format!("{err:?}").contains("extrude(box,side)"), "{err:?}");
775 }
776
777 #[test]
780 fn duplicate_step_ids_are_rejected() {
781 let mut program = box_with_drill_hole();
782 program.steps[3].id = "box".into();
783 assert!(program.apply(Part::<S>::new()).is_err());
784 }
785
786 #[test]
796 fn handle_with_hole_round_trips() {
797 build_and_round_trip("handle_with_hole", &handle_with_hole());
798 }
799
800 #[test]
801 fn luggage_tag_round_trips() {
802 build_and_round_trip("luggage_tag", &luggage_tag());
803 }
804}