Another project
1use bone_document::{
2 DimensionKind, Document, DocumentFolder, DocumentHeader, ExtrudeFile, Sketch, SketchDimension,
3 SketchEdit, SketchEntity, SketchFile, SketchRelation, save, to_string,
4};
5use bone_kernel::{
6 ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
7};
8use bone_types::{
9 Angle, DocumentId, Length, Point2, Point3, PositiveLength, SketchEntityId, SketchId,
10 SketchPlaneBasis, Tolerance, UnitVec3, degree, millimeter,
11};
12use slotmap::KeyData;
13use tempfile::{TempDir, tempdir};
14
15fn plane() -> SketchPlaneBasis {
16 let Ok(basis) = SketchPlaneBasis::new(
17 Point3::origin(),
18 UnitVec3::x_axis(),
19 UnitVec3::y_axis(),
20 Tolerance::new(1e-9),
21 ) else {
22 panic!("xy plane");
23 };
24 basis
25}
26
27fn mm(v: f64) -> Length {
28 Length::new::<millimeter>(v)
29}
30
31fn deg(v: f64) -> Angle {
32 Angle::new::<degree>(v)
33}
34
35fn ok_dir() -> TempDir {
36 let Ok(dir) = tempdir() else {
37 panic!("tempdir");
38 };
39 dir
40}
41
42fn sketch_id(idx: u32) -> SketchId {
43 SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
44}
45
46fn document_id(idx: u32) -> DocumentId {
47 DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
48}
49
50fn point_of(s: &Sketch, i: usize) -> SketchEntityId {
51 s.entity_order()[i]
52}
53
54fn add_entity(s: Sketch, entity: SketchEntity) -> Sketch {
55 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(entity)) else {
56 panic!("add entity");
57 };
58 next
59}
60
61fn add_relation(s: Sketch, relation: SketchRelation) -> Sketch {
62 let Ok((next, _)) = s.apply(SketchEdit::AddRelation(relation)) else {
63 panic!("add relation");
64 };
65 next
66}
67
68fn add_dimension(s: Sketch, dim: SketchDimension) -> Sketch {
69 let Ok((next, _)) = s.apply(SketchEdit::AddDimension(dim)) else {
70 panic!("add dim");
71 };
72 next
73}
74
75fn first_of_kind(s: &Sketch, kind: bone_document::SketchEntityKind) -> SketchEntityId {
76 let Some(&id) = s.entity_order().iter().find(|id| {
77 matches!(
78 s.entities().get(**id).map(bone_document::SketchEntity::kind),
79 Some(k) if k == kind
80 )
81 }) else {
82 panic!("no entity of kind {kind:?}");
83 };
84 id
85}
86
87fn build_full_sketch() -> Sketch {
88 let coords = [
89 Point2::from_mm(0.0, 0.0),
90 Point2::from_mm(10.0, 0.0),
91 Point2::from_mm(10.0, 5.0),
92 Point2::from_mm(0.0, 5.0),
93 ];
94 let with_points = coords.into_iter().fold(Sketch::new(plane()), |s, p| {
95 add_entity(s, SketchEntity::point(p))
96 });
97 let p0 = point_of(&with_points, 0);
98 let p1 = point_of(&with_points, 1);
99 let p2 = point_of(&with_points, 2);
100 let p3 = point_of(&with_points, 3);
101
102 let with_lines = [
103 (p0, p1, false),
104 (p1, p2, false),
105 (p2, p3, false),
106 (p3, p0, true),
107 ]
108 .into_iter()
109 .fold(with_points, |s, (a, b, cons)| {
110 add_entity(s, SketchEntity::line(a, b, cons))
111 });
112 let with_arc = add_entity(with_lines, SketchEntity::arc(p0, p1, p2, false));
113 let with_circle = add_entity(with_arc, SketchEntity::circle(p0, mm(2.5), false));
114
115 let line_ids: Vec<_> = with_circle
116 .entity_order()
117 .iter()
118 .copied()
119 .filter(|id| {
120 matches!(
121 with_circle
122 .entities()
123 .get(*id)
124 .map(bone_document::SketchEntity::kind),
125 Some(bone_document::SketchEntityKind::Line)
126 )
127 })
128 .collect();
129 let arc_id = first_of_kind(&with_circle, bone_document::SketchEntityKind::Arc);
130 let circle_id = first_of_kind(&with_circle, bone_document::SketchEntityKind::Circle);
131
132 let relations = [
133 SketchRelation::Coincident(p0, line_ids[0]),
134 SketchRelation::Horizontal(line_ids[0]),
135 SketchRelation::Vertical(line_ids[1]),
136 SketchRelation::Parallel(line_ids[0], line_ids[2]),
137 SketchRelation::Perpendicular(line_ids[0], line_ids[1]),
138 SketchRelation::Tangent(line_ids[0], circle_id),
139 SketchRelation::Equal(line_ids[0], line_ids[2]),
140 SketchRelation::Concentric(arc_id, circle_id),
141 SketchRelation::Midpoint {
142 point: p2,
143 line: line_ids[0],
144 },
145 SketchRelation::Fix(p0),
146 ];
147 let with_relations = relations.into_iter().fold(with_circle, add_relation);
148
149 let dimensions = [
150 SketchDimension::Linear {
151 a: p0,
152 b: p1,
153 value: mm(10.0),
154 kind: DimensionKind::Driving,
155 },
156 SketchDimension::Radius {
157 target: circle_id,
158 value: mm(2.5),
159 kind: DimensionKind::Driven,
160 },
161 SketchDimension::Diameter {
162 target: circle_id,
163 value: mm(5.0),
164 kind: DimensionKind::Driving,
165 },
166 SketchDimension::Angular {
167 a: line_ids[0],
168 b: line_ids[1],
169 value: deg(90.0),
170 kind: DimensionKind::Driving,
171 },
172 ];
173 dimensions.into_iter().fold(with_relations, add_dimension)
174}
175
176fn assert_ron<T: serde::Serialize>(value: &T) -> String {
177 let Ok(ron) = to_string(value) else {
178 panic!("ron");
179 };
180 ron
181}
182
183#[test]
184fn document_header_ron_surface() {
185 let mut doc = Document::new(document_id(1), "demo".to_owned());
186 let sid = sketch_id(7);
187 doc.insert_sketch(sid, "Sketch1".to_owned(), build_full_sketch());
188 doc.set_parameter("width".to_owned(), 10.0);
189
190 let ron = assert_ron(doc.header());
191 insta::assert_snapshot!("document_header", ron);
192}
193
194#[test]
195fn sketch_file_ron_surface() {
196 let file = SketchFile::new(build_full_sketch());
197 let ron = assert_ron(&file);
198 insta::assert_snapshot!("sketch_file", ron);
199}
200
201#[test]
202fn extrude_file_ron_surface() {
203 let Ok(depth) = PositiveLength::new(mm(10.0)) else {
204 panic!("positive depth");
205 };
206 let file = ExtrudeFile::new(
207 ExtrudeFeature {
208 sketch: sketch_id(7),
209 direction: ExtrudeDirection::Normal {
210 sense: ExtrudeSense::Forward,
211 },
212 end_condition: ExtrudeEndCondition::Blind { depth },
213 draft: None,
214 thin_wall: None,
215 merge_result: MergeResult::Merge,
216 },
217 "Extrude1".to_owned(),
218 );
219 let ron = assert_ron(&file);
220 insta::assert_snapshot!("extrude_file", ron);
221}
222
223#[test]
224fn saved_folder_layout_is_stable() {
225 let dir = ok_dir();
226 let folder = DocumentFolder::new(dir.path().join("pinned.bone"));
227 let mut doc = Document::new(document_id(1), "pinned".to_owned());
228 doc.insert_sketch(sketch_id(1), "Rect".to_owned(), build_full_sketch());
229 let Ok(()) = save(&doc, &folder) else {
230 panic!("save");
231 };
232
233 let mut entries: Vec<String> = walk_relative(folder.path()).collect();
234 entries.sort();
235 let listing = entries.join("\n");
236 insta::assert_snapshot!("folder_listing", listing);
237}
238
239fn walk_relative(root: &std::path::Path) -> impl Iterator<Item = String> + '_ {
240 fn walk(root: &std::path::Path, path: &std::path::Path, out: &mut Vec<String>) {
241 let Ok(iter) = std::fs::read_dir(path) else {
242 return;
243 };
244 iter.flatten().for_each(|entry| {
245 let p = entry.path();
246 let rel = p
247 .strip_prefix(root)
248 .unwrap_or(&p)
249 .to_string_lossy()
250 .into_owned();
251 if p.is_dir() {
252 out.push(format!("{rel}/"));
253 walk(root, &p, out);
254 } else {
255 out.push(rel);
256 }
257 });
258 }
259 let mut out = Vec::new();
260 walk(root, root, &mut out);
261 out.into_iter()
262}
263
264#[test]
265fn document_header_roundtrips_through_ron() {
266 let mut doc = Document::new(document_id(1), "rt".to_owned());
267 doc.insert_sketch(sketch_id(1), "S".to_owned(), build_full_sketch());
268 let ron = assert_ron(doc.header());
269 let Ok(parsed) = bone_document::from_str::<DocumentHeader>(&ron) else {
270 panic!("parse");
271 };
272 assert_eq!(&parsed, doc.header());
273}