Another project
1use std::process::Command;
2
3use bone_document::{
4 BlobKind, DimensionKind, Document, DocumentFolder, EditOutcome, Sketch, SketchDimension,
5 SketchEdit, SketchEntity, evaluate_extrude, evaluate_sketch, load, save, write_solid,
6};
7use bone_kernel::{
8 BrepSolid, ExtrudeDirection, ExtrudeEndCondition, ExtrudeFeature, ExtrudeSense, MergeResult,
9};
10use bone_types::{
11 DocumentId, ExtrudeId, Length, Point2, Point3, PositiveLength, SketchId, SketchPlaneBasis,
12 Tolerance, UnitVec3, millimeter,
13};
14use slotmap::KeyData;
15use tempfile::{TempDir, tempdir};
16
17fn plane() -> SketchPlaneBasis {
18 let Ok(basis) = SketchPlaneBasis::new(
19 Point3::origin(),
20 UnitVec3::x_axis(),
21 UnitVec3::y_axis(),
22 Tolerance::new(1e-9),
23 ) else {
24 panic!("xy plane");
25 };
26 basis
27}
28
29fn mm(v: f64) -> Length {
30 Length::new::<millimeter>(v)
31}
32
33fn ok_dir() -> TempDir {
34 let Ok(dir) = tempdir() else {
35 panic!("tempdir");
36 };
37 dir
38}
39
40fn sketch_id(idx: u32) -> SketchId {
41 SketchId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
42}
43
44fn document_id(idx: u32) -> DocumentId {
45 DocumentId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
46}
47
48fn jj_available() -> bool {
49 let Ok(out) = Command::new("jj").arg("--version").output() else {
50 return false;
51 };
52 out.status.success()
53}
54
55fn run(cmd: &mut Command) -> String {
56 let Ok(out) = cmd.output() else {
57 panic!("{cmd:?}");
58 };
59 assert!(
60 out.status.success(),
61 "{cmd:?} failed: {}\n{}",
62 String::from_utf8_lossy(&out.stdout),
63 String::from_utf8_lossy(&out.stderr),
64 );
65 String::from_utf8_lossy(&out.stdout).into_owned()
66}
67
68fn rectangle() -> Sketch {
69 let script = [
70 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 0.0))),
71 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 0.0))),
72 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(10.0, 5.0))),
73 SketchEdit::AddEntity(SketchEntity::point(Point2::from_mm(0.0, 5.0))),
74 ];
75 let Ok((s, _)) = Sketch::new(plane()).apply_all(script) else {
76 panic!("rectangle");
77 };
78 s
79}
80
81fn assert_save(doc: &Document, folder: &DocumentFolder) {
82 let Ok(()) = save(doc, folder) else {
83 panic!("save");
84 };
85}
86
87fn assert_load(folder: &DocumentFolder) -> Document {
88 let Ok(doc) = load(folder) else {
89 panic!("load");
90 };
91 doc
92}
93
94#[test]
95fn jj_diff_on_dimension_edit_is_text_only() {
96 if !jj_available() {
97 eprintln!("skip: jj not on PATH");
98 return;
99 }
100
101 let dir = ok_dir();
102 let folder = DocumentFolder::new(dir.path().join("demo.bone"));
103
104 let mut doc = Document::new(document_id(1), "demo".to_owned());
105 let sid = sketch_id(1);
106 let rect = rectangle();
107 let Some(&a) = rect.entity_order().first() else {
108 panic!("rect has points");
109 };
110 let Some(&b) = rect.entity_order().get(1) else {
111 panic!("rect has points");
112 };
113 let Ok((rect, EditOutcome::Dimension(dim_id))) =
114 rect.apply(SketchEdit::AddDimension(SketchDimension::Linear {
115 a,
116 b,
117 value: mm(10.0),
118 kind: DimensionKind::Driving,
119 }))
120 else {
121 panic!("dim");
122 };
123 doc.insert_sketch(sid, "Sketch1".to_owned(), rect.clone());
124 assert_save(&doc, &folder);
125
126 run(Command::new("jj")
127 .arg("git")
128 .arg("init")
129 .arg("--colocate")
130 .current_dir(folder.path()));
131 run(Command::new("jj")
132 .arg("config")
133 .arg("set")
134 .arg("--repo")
135 .arg("user.name")
136 .arg("test")
137 .current_dir(folder.path()));
138 run(Command::new("jj")
139 .arg("config")
140 .arg("set")
141 .arg("--repo")
142 .arg("user.email")
143 .arg("test@example.com")
144 .current_dir(folder.path()));
145 run(Command::new("jj")
146 .arg("describe")
147 .arg("--message")
148 .arg("initial")
149 .current_dir(folder.path()));
150 run(Command::new("jj").arg("new").current_dir(folder.path()));
151
152 let reopened = assert_load(&folder);
153 let Some(sketch) = reopened.sketch(sid) else {
154 panic!("sketch missing");
155 };
156 let Ok((updated_sketch, _)) = sketch.clone().apply(SketchEdit::UpdateDimensionValue {
157 id: dim_id,
158 value: bone_document::DimensionValue::Length(mm(12.5)),
159 }) else {
160 panic!("update dim");
161 };
162 let mut next_doc = reopened.clone();
163 next_doc.replace_sketch(sid, updated_sketch);
164 assert_save(&next_doc, &folder);
165
166 let diff = run(Command::new("jj")
167 .arg("diff")
168 .arg("--git")
169 .current_dir(folder.path()));
170 assert!(
171 diff.contains("sketches/"),
172 "expected sketch file in diff:\n{diff}"
173 );
174 assert!(
175 !diff.contains("document.ron"),
176 "a dimension edit must leave the recipe stable, only the sketch file changes:\n{diff}"
177 );
178 assert!(
179 diff.contains('+') && diff.contains('-'),
180 "expected text diff markers:\n{diff}"
181 );
182 assert!(
183 !diff.contains("Binary files"),
184 "expected no binary diff:\n{diff}"
185 );
186}
187
188fn extrude_id(idx: u32) -> ExtrudeId {
189 ExtrudeId::from(KeyData::from_ffi((1u64 << 32) | u64::from(idx)))
190}
191
192fn add_point(s: Sketch, x: f64, y: f64) -> (Sketch, bone_types::SketchEntityId) {
193 let Ok((next, EditOutcome::Entity(id))) = s.apply(SketchEdit::AddEntity(SketchEntity::point(
194 Point2::from_mm(x, y),
195 ))) else {
196 panic!("add point");
197 };
198 (next, id)
199}
200
201fn closed_rectangle() -> Sketch {
202 let s = Sketch::new(plane());
203 let (s, p0) = add_point(s, 0.0, 0.0);
204 let (s, p1) = add_point(s, 10.0, 0.0);
205 let (s, p2) = add_point(s, 10.0, 5.0);
206 let (s, p3) = add_point(s, 0.0, 5.0);
207 [(p0, p1), (p1, p2), (p2, p3), (p3, p0)]
208 .into_iter()
209 .fold(s, |s, (a, b)| {
210 let Ok((next, _)) = s.apply(SketchEdit::AddEntity(SketchEntity::line(a, b, false)))
211 else {
212 panic!("rectangle edge");
213 };
214 next
215 })
216}
217
218fn blind_extrude(sketch: SketchId, depth_mm: f64) -> ExtrudeFeature {
219 let Ok(depth) = PositiveLength::new(mm(depth_mm)) else {
220 panic!("positive depth");
221 };
222 ExtrudeFeature {
223 sketch,
224 direction: ExtrudeDirection::Normal {
225 sense: ExtrudeSense::Forward,
226 },
227 end_condition: ExtrudeEndCondition::Blind { depth },
228 draft: None,
229 thin_wall: None,
230 merge_result: MergeResult::Merge,
231 }
232}
233
234fn evaluated_solid(doc: &Document, sketch: SketchId, extrude: ExtrudeId) -> BrepSolid {
235 let Some(sketch_value) = doc.sketch(sketch) else {
236 panic!("sketch present");
237 };
238 let Some(feature_id) = doc.feature_tree().feature_of_extrude(extrude) else {
239 panic!("extrude node present");
240 };
241 let Some(&feature) = doc.extrude_of_feature(feature_id) else {
242 panic!("extrude feature present");
243 };
244 let evaluated = evaluate_sketch(sketch_value);
245 let extruded = evaluate_extrude(feature_id, &evaluated, &feature);
246 let Some(solid) = extruded.solid() else {
247 panic!("rectangle extrudes to a solid");
248 };
249 solid.clone()
250}
251
252#[test]
253fn jj_diff_on_extrude_edit_is_text_only() {
254 if !jj_available() {
255 eprintln!("skip: jj not on PATH");
256 return;
257 }
258
259 let dir = ok_dir();
260 let folder = DocumentFolder::new(dir.path().join("slab.bone"));
261 let sid = sketch_id(1);
262 let eid = extrude_id(1);
263
264 let mut doc = Document::new(document_id(1), "slab".to_owned());
265 doc.insert_sketch(sid, "Sketch1".to_owned(), closed_rectangle());
266 doc.insert_extrude(eid, blind_extrude(sid, 10.0));
267 assert_save(&doc, &folder);
268 let Ok(_baseline_blob) = write_solid(&folder, &evaluated_solid(&doc, sid, eid)) else {
269 panic!("cache the depth-10 solid");
270 };
271
272 run(Command::new("jj")
273 .arg("git")
274 .arg("init")
275 .arg("--colocate")
276 .current_dir(folder.path()));
277 run(Command::new("jj")
278 .arg("config")
279 .arg("set")
280 .arg("--repo")
281 .arg("user.name")
282 .arg("test")
283 .current_dir(folder.path()));
284 run(Command::new("jj")
285 .arg("config")
286 .arg("set")
287 .arg("--repo")
288 .arg("user.email")
289 .arg("test@example.com")
290 .current_dir(folder.path()));
291 run(Command::new("jj")
292 .arg("describe")
293 .arg("--message")
294 .arg("initial")
295 .current_dir(folder.path()));
296 run(Command::new("jj").arg("new").current_dir(folder.path()));
297
298 let document_before = std::fs::read_to_string(folder.document_file()).unwrap_or_default();
299
300 let mut next = assert_load(&folder);
301 next.insert_extrude(eid, blind_extrude(sid, 14.0));
302 assert_save(&next, &folder);
303 let Ok(edited_blob) = write_solid(&folder, &evaluated_solid(&next, sid, eid)) else {
304 panic!("cache the depth-14 solid");
305 };
306
307 let document_after = std::fs::read_to_string(folder.document_file()).unwrap_or_default();
308 assert_eq!(
309 document_before, document_after,
310 "an extrude-depth edit must not churn document.ron; geometry is not in the recipe"
311 );
312 assert!(
313 folder.blob_path(edited_blob, BlobKind::BREP).exists(),
314 "the widened slab content-addresses a fresh brep blob"
315 );
316
317 let diff = run(Command::new("jj")
318 .arg("diff")
319 .arg("--git")
320 .current_dir(folder.path()));
321 assert!(
322 diff.contains("extrudes/"),
323 "expected the extrude file in the diff:\n{diff}"
324 );
325 assert!(
326 diff.contains(".brep"),
327 "expected a new geometry blob in the diff:\n{diff}"
328 );
329 assert!(
330 diff.contains('+') && diff.contains('-'),
331 "expected text diff markers:\n{diff}"
332 );
333 assert!(
334 !diff.contains("Binary files"),
335 "expected no binary diff:\n{diff}"
336 );
337}