feat: strengthen sketches, feature editing, and independent assemblies

This commit is contained in:
bherbruck committed 2026-10-11 02:44:31 -04:00
1 parent 07932d0f42
commit 0398a813c8
65 files changed
+7082 -268

No files matched your search

+175
View File
@@ -8,12 +8,187 @@ use solvecraft_engine::{Sel, Session};
use crate::dialogs::{Dialog, Kind, apply_commands};
use crate::{Services, SolveApp};
#[test]
fn cylinder_emboss_target_face_and_edit_keep_the_anchored_operation() {
let mut h = crate::scenario::Harness::new();
h.app.run("solid.cylinder", json!({"radius":10,"height":20,"body_name":"Sleeve"})).unwrap();
let sketch = h.app.run("sketch.create", json!({"plane":{"origin":[0,10,0],"x_dir":[-1,0,0],"y_dir":[0,0,1]},"name":"Badge"})).unwrap()["sketch"]
.as_u64()
.unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.session.selection = vec![Sel::Profile { sketch, index: 0 }];
h.app.start("solid.emboss");
h.frames(2);
assert_eq!(h.app.dialog.as_ref().unwrap().inputs[0].items.len(), 1);
let planar = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
assert!(planar[0].1.get("face").is_none());
h.call("ui.dialogInput", json!({"index":1}));
h.call("ui.view", json!({"view":"back"}));
h.frames(2);
h.step(&json!({"click":{"world":[0,10,7.5]}})).unwrap();
let dialog = h.app.dialog.as_ref().unwrap();
assert!(matches!(dialog.inputs[1].items.first(), Some(Sel::Face { body, .. }) if body == "Sleeve"));
let commands = apply_commands(&h.app, dialog).unwrap();
assert_eq!(commands[0].1["targets"], json!(["Sleeve"]));
assert!(commands[0].1.get("face").is_some());
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
let feature = h.app.session.doc.features.last().unwrap().id;
let before = serde_json::to_value(&h.app.session.doc.feature(feature).unwrap().kind).unwrap();
h.app.edit_feature(feature);
h.frames(2);
let edited = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
assert_eq!(edited[0].0, "timeline.redefine");
assert_eq!(edited[0].1["params"]["targets"], json!(["Sleeve"]));
assert!(edited[0].1["params"].get("face").is_some());
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
assert_eq!(serde_json::to_value(&h.app.session.doc.feature(feature).unwrap().kind).unwrap(), before);
}
#[test]
fn cylinder_emboss_edit_retains_second_source_and_author_paths_and_all_profiles() {
let mut h = crate::scenario::Harness::new();
let source = h.app.run("component.create", json!({"name":"Source"})).unwrap()["component"].as_u64().unwrap();
h.app.run("solid.cylinder", json!({"radius":10,"height":20,"body_name":"Sleeve"})).unwrap();
h.app.run("occurrence.move", json!({"occurrence":"Source:1","axis":[0,0,1],"angle":"90 deg","translate":[50,0,0]})).unwrap();
let source_second = h.app.run("occurrence.copy", json!({"component":source,"translate":[80,0,0]})).unwrap()["occurrence"].as_u64().unwrap();
h.app.run("component.activate", json!({"component":"root"})).unwrap();
let author = h.app.run("component.create", json!({"name":"Author"})).unwrap()["component"].as_u64().unwrap();
h.app.run("occurrence.move", json!({"occurrence":"Author:1","axis":[1,0,0],"angle":"90 deg","translate":[-100,40,30]})).unwrap();
let author_second = h.app.run("occurrence.copy", json!({"component":author,"translate":[0,100,0]})).unwrap()["occurrence"].as_u64().unwrap();
h.app.run("component.activate", json!({"component":author,"occurrence":author_second})).unwrap();
h.app.run("sketch.create", json!({"plane":{"origin":[120,0,0],"x_dir":[0,-1,0],"y_dir":[0,0,1]},"name":"Badge"})).unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
let target = h.app.session.occurrence_bodies(&[source_second], false).unwrap().into_iter().next().unwrap();
let feature = h.app.run("solid.emboss", json!({"sketch":"Badge","profiles":"all","face":[120,0,7.5],"depth":"1mm","targets":[target]})).unwrap()
["feature"]
.as_u64()
.unwrap();
let before = h.app.session.doc.feature(feature).unwrap().clone();
let solvecraft_engine::doc::FeatureKind::Emboss { face: Some(face), .. } = &before.kind else { panic!("selected face") };
assert_eq!(face.path, vec![source_second]);
assert_eq!(face.author_path, vec![author_second]);
h.app.run("component.activate", json!({"component":"root"})).unwrap();
h.app.edit_feature(feature);
h.frames(2);
let dialog = h.app.dialog.as_ref().unwrap();
let Sel::Face { body, point, .. } = &dialog.inputs[1].items[0] else { panic!("refilled face") };
assert_eq!(body, &target);
assert!(point.dist(Vec3::new(120.0, 0.0, 7.5)) < 1e-8);
let commands = apply_commands(&h.app, dialog).unwrap();
assert_eq!(commands[0].1["params"]["profiles"], json!("all"));
let Sel::Profile { sketch, index } = dialog.inputs[0].items[0] else { panic!("refilled profile") };
let plane = crate::dialogs::emboss_profile_plane(&h.app, sketch).unwrap().unwrap();
assert!(plane.origin.dist(Vec3::new(120.0, 0.0, 0.0)) < 1e-8, "{plane:?}");
let mut guide = crate::gpu::GpuScene::default();
crate::viewport::profile_fill(&mut guide, &h.app, sketch, index, egui::Color32::BLUE, egui::Color32::BLUE);
let (vertices, remainder) = guide.trans.as_chunks::<{ crate::gpu::TRI_SIZE }>();
assert!(remainder.is_empty() && !vertices.is_empty());
let lift = h.app.cam.back() * (h.app.cam.half_height() * 0.002);
for vertex in vertices {
let coordinate = |offset| f32::from_le_bytes(vertex[offset..offset + 4].try_into().unwrap()) as f64;
let point = Vec3::new(coordinate(0), coordinate(4), coordinate(8)) - lift;
assert!((point.x - 120.0).abs() < 1e-4 && point.y.abs() < 2.01 && (4.99..=10.01).contains(&point.z), "{point:?}");
}
let preview = h.app.session.preview_world(&commands).unwrap();
let volume = preview.after.body(&target).unwrap().mesh().measure().volume;
assert!(volume > preview.before.body(&target).unwrap().mesh().measure().volume + 10.0);
let colors = crate::preview::Colors::from_tokens(&crate::theme::Tokens::get());
let built = crate::preview::compute(&h.app.session, &commands, colors).unwrap();
assert!(built.replaced.contains(&target) && !built.scene.tris.is_empty(), "{:?}", built.replaced);
let source_first = h.app.session.doc.occurrence_of(source).unwrap().id;
h.app.run("browser.visibility", json!({"paths":[[source_first]],"visible":false})).unwrap();
let hidden = crate::preview::compute(&h.app.session, &commands, colors).unwrap();
assert_eq!(hidden.replaced, vec![target.clone()]);
let visible = h.app.session.preview_visible_world(&commands).unwrap();
assert!(visible.before.body("Sleeve").is_none() && visible.after.body("Sleeve").is_none());
assert!(visible.after.body(&target).is_some() && !hidden.scene.tris.is_empty());
let mut shared = h.app.session.scratch();
shared.execute("component.activate", &json!({"component":source,"occurrence":source_first})).unwrap();
let new_body = shared
.preview_visible_world(&[("solid.box".into(), json!({"body_name":"NewPreview","length":2,"width":2,"height":2,"operation":"new"}))])
.unwrap();
let added = new_body.after.bodies.iter().filter(|body| new_body.before.body(&body.name).is_none()).collect::<Vec<_>>();
assert_eq!(added.len(), 1, "{:?}", added.iter().map(|body| &body.name).collect::<Vec<_>>());
assert!(added[0].name.contains("Source:2"), "{}", added[0].name);
let doc = h.app.session.doc.to_json();
let history = (h.app.session.undo.len(), h.app.session.redo.len());
let selection = h.app.session.selection.clone();
let Kind::Emboss { depth, .. } = &mut h.app.dialog.as_mut().unwrap().kind else { panic!("Emboss editor") };
*depth = "2 mm".into();
let changed = apply_commands(&h.app, h.app.dialog.as_ref().unwrap()).unwrap();
let changed_preview = h.app.session.preview_world(&changed).unwrap();
assert!(changed_preview.after.body(&target).unwrap().mesh().measure().volume > volume + 10.0);
let mut invalid = changed.clone();
invalid[0].1["params"]["depth"] = json!("unknown_depth");
assert!(h.app.session.preview_world(&invalid).is_err());
assert_eq!(h.app.session.doc.to_json(), doc);
assert_eq!((h.app.session.undo.len(), h.app.session.redo.len()), history);
assert_eq!(h.app.session.selection, selection);
h.step(&json!({"key":"Escape"})).unwrap();
assert!(h.app.dialog.is_none());
assert_eq!(h.app.session.doc.feature(feature).unwrap().kind, before.kind);
h.app.edit_feature(feature);
h.frames(2);
h.step(&json!({"key":"Enter"})).unwrap();
assert!(h.app.dialog.is_none(), "{:?}", h.app.dialog);
let after = h.app.session.doc.feature(feature).unwrap();
assert_eq!(after.kind, before.kind);
assert_eq!(after.face_names, before.face_names);
assert_eq!(h.app.session.active_component, 0);
}
fn app_with(script: serde_json::Value) -> SolveApp {
let mut s = Session::default();
s.run_script(&script).unwrap();
SolveApp::new(s, Services::default())
}
#[test]
fn cylinder_emboss_profile_guide_uses_target_or_unique_foreign_sketch_frame() {
for target_owned in [true, false] {
let mut h = crate::scenario::Harness::new();
let source = if target_owned { h.app.run("component.create", json!({"name":"Source"})).unwrap()["component"].as_u64().unwrap() } else { 0 };
h.app.run("solid.cylinder", json!({"radius":10,"height":20,"body_name":"Sleeve"})).unwrap();
let (profile_owner, expected, target, face) = if target_owned {
h.app.run("occurrence.move", json!({"occurrence":"Source:1","axis":[0,0,1],"angle":"90 deg","translate":[50,0,0]})).unwrap();
let second = h.app.run("occurrence.copy", json!({"component":source,"translate":[80,0,0]})).unwrap()["occurrence"].as_u64().unwrap();
let target = h.app.session.occurrence_bodies(&[second], false).unwrap().remove(0);
(source, Vec3::new(120.0, 0.0, 0.0), target, json!([120, 0, 7.5]))
} else {
let profile = h.app.run("component.create", json!({"name":"Profile"})).unwrap()["component"].as_u64().unwrap();
h.app.run("occurrence.move", json!({"occurrence":"Profile:1","axis":[0,0,1],"angle":"90 deg","translate":[50,20,0]})).unwrap();
(profile, Vec3::new(0.0, 10.0, 0.0), "Sleeve".to_string(), json!([0, 10, 7.5]))
};
let plane = if target_owned {
json!({"origin":[40,0,0],"x_dir":[0,-1,0],"y_dir":[0,0,1]})
} else {
json!({"origin":[0,10,0],"x_dir":[-1,0,0],"y_dir":[0,0,1]})
};
let sketch = h.app.run("sketch.create", json!({"plane":plane,"name":"Badge"})).unwrap()["sketch"].as_u64().unwrap();
h.app.run("sketch.rectangle.two_point", json!({"p0":[-2,5],"p1":[2,10]})).unwrap();
h.app.run("sketch.finish", json!({})).unwrap();
h.app.run("component.activate", json!({"component":"root"})).unwrap();
let feature =
h.app.run("solid.emboss", json!({"sketch":sketch,"profiles":"all","face":face,"depth":"1mm","targets":[target]})).unwrap()["feature"]
.as_u64()
.unwrap();
h.app.edit_feature(feature);
h.frames(2);
assert!(crate::dialogs::emboss_profile_plane(&h.app, sketch).unwrap().unwrap().origin.dist(expected) < 1e-8);
if !target_owned {
h.app.run("occurrence.copy", json!({"component":profile_owner,"translate":[100,0,0]})).unwrap();
assert!(crate::dialogs::emboss_profile_plane(&h.app, sketch).unwrap_err().contains("ambiguous"));
let mut guide = crate::gpu::GpuScene::default();
crate::viewport::profile_fill(&mut guide, &h.app, sketch, 0, egui::Color32::BLUE, egui::Color32::BLUE);
assert!(guide.trans.is_empty() && guide.lines.is_empty());
}
}
}
fn boxed() -> SolveApp {
app_with(json!([{"command": "solid.box", "params": {"length": 40, "width": 30, "height": 20}}]))
}