A sketch's link to a projected edge now keeps the edge's persistent name and finds it by that name when the body changes, instead of taking whichever edge ends up nearest its point.
229 lines
12 KiB
Rust
229 lines
12 KiB
Rust
//! A projected edge stays the edge that was picked: its link keeps the edge's persistent name,
|
|
//! so the edge is found by that name wherever a change of the body takes it, and no other edge
|
|
//! is taken for it when it is split or gone.
|
|
|
|
use serde_json::{Value, json};
|
|
use solidcraft_geom::{Vec2, Vec3};
|
|
use solidcraft_sketch::{Link, LinkSource, Shape};
|
|
|
|
use crate::Session;
|
|
|
|
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
|
match s.execute(id, &p) {
|
|
Ok(v) => v,
|
|
Err(e) => panic!("{id} {p}: {e}"),
|
|
}
|
|
}
|
|
|
|
/// A plate `width` (100) x 50 x 20 with a corner at the origin: body `Body1`.
|
|
fn plate(s: &mut Session) {
|
|
run(s, "parameters.add", json!({"name": "width", "expression": "100 mm"}));
|
|
run(s, "sketch.create", json!({"plane": "XY", "name": "Base"}));
|
|
run(s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [100, 50]}));
|
|
run(s, "sketch.dimension", json!({"entities": ["l1"], "value": "width"}));
|
|
run(s, "sketch.finish", json!({}));
|
|
run(s, "solid.extrude", json!({"sketch": "Base", "distance": "20 mm", "name": "Plate"}));
|
|
}
|
|
|
|
/// Sketch `Top` on the plate's top (z = 20) with the top edge of the plate's right-hand side,
|
|
/// from (100, 0, 20) to (100, 50, 20), projected into it: picked at `y` along it.
|
|
fn project_right_edge(s: &mut Session, y: f64) {
|
|
run(s, "sketch.create", json!({"plane": "XY", "offset": "20 mm", "name": "Top"}));
|
|
run(s, "sketch.project", json!({"refs": [{"edge": [100, y, 20], "body": "Body1"}]}));
|
|
run(s, "sketch.finish", json!({}));
|
|
}
|
|
|
|
fn set_width(s: &mut Session, width: f64) {
|
|
run(s, "parameters.change", json!({"name": "width", "expression": format!("{width} mm")}));
|
|
}
|
|
|
|
/// What sketch `Top` has to report.
|
|
fn trouble(s: &Session) -> String {
|
|
let r = s.model.result(s.doc.find_feature("Top").unwrap().id).unwrap();
|
|
format!("{}{}", r.error.clone().unwrap_or_default(), r.warning.clone().unwrap_or_default())
|
|
}
|
|
|
|
/// The link of sketch `Top` as the model has it.
|
|
fn link(s: &Session) -> Link {
|
|
s.model.state().sketch(s.doc.find_feature("Top").unwrap().id).unwrap().sketch.links[0].clone()
|
|
}
|
|
|
|
/// The link of sketch `Top` as the design stores it.
|
|
fn stored(s: &Session) -> Link {
|
|
s.doc.sketch(s.doc.find_feature("Top").unwrap().id).unwrap().links[0].clone()
|
|
}
|
|
|
|
/// The ends of the line the link of sketch `Top` gives.
|
|
fn projected(s: &Session) -> (Vec2, Vec2) {
|
|
let st = s.model.state();
|
|
let sk = &st.sketch(s.doc.find_feature("Top").unwrap().id).unwrap().sketch;
|
|
match sk.shape(sk.link_curves(&sk.links[0].id)[0]) {
|
|
Some(Shape::Line { a, b }) => (a, b),
|
|
other => panic!("not a line: {other:?}"),
|
|
}
|
|
}
|
|
|
|
/// The projected line runs along y at `x`, between `y0` and `y1`.
|
|
#[track_caller]
|
|
fn assert_line(s: &Session, x: f64, (y0, y1): (f64, f64), when: &str) {
|
|
let (a, b) = projected(s);
|
|
let ends = (a.y.min(b.y), a.y.max(b.y));
|
|
assert!(
|
|
(a.x - x).abs() < 1e-6 && (b.x - x).abs() < 1e-6 && (ends.0 - y0).abs() < 1e-6 && (ends.1 - y1).abs() < 1e-6,
|
|
"{when}: expected the line x = {x}, y = {y0}..{y1}: {a:?} {b:?}"
|
|
);
|
|
}
|
|
|
|
/// The name of the edge of `Body1` whose middle is `mid`.
|
|
fn edge_named_at(s: &mut Session, mid: [f64; 3]) -> String {
|
|
let edges = run(s, "model.edges", json!({"body": "Body1"}));
|
|
let at = |e: &Value| Vec3::new(e["mid"][0].as_f64().unwrap(), e["mid"][1].as_f64().unwrap(), e["mid"][2].as_f64().unwrap());
|
|
let edge = edges["edges"].as_array().unwrap().iter().find(|e| at(e).dist(Vec3::new(mid[0], mid[1], mid[2])) < 1e-6).unwrap();
|
|
edge["name"].as_str().unwrap().to_string()
|
|
}
|
|
|
|
/// The picked point (100, 25, 20) is 25 from the front and back edges of the top: wider than
|
|
/// 125 they are nearer to it than the picked edge is. At 60 the picked edge is 40 from it,
|
|
/// further than a reference found by its point is looked for (a quarter of the body's diagonal,
|
|
/// 20.2). The link follows the edge by its name through all of them.
|
|
#[test]
|
|
fn a_projected_edge_stays_that_edge_when_the_body_changes_size() {
|
|
let mut s = Session::default();
|
|
plate(&mut s);
|
|
project_right_edge(&mut s, 25.0);
|
|
assert_line(&s, 100.0, (0.0, 50.0), "drawn");
|
|
let name = edge_named_at(&mut s, [100.0, 25.0, 20.0]);
|
|
assert_eq!(stored(&s).edge_name.as_deref(), Some(name.as_str()));
|
|
for width in [150.0, 60.0, 400.0] {
|
|
set_width(&mut s, width);
|
|
assert_eq!(trouble(&s), "", "width {width}");
|
|
assert_line(&s, width, (0.0, 50.0), &format!("width {width}"));
|
|
assert!(!link(&s).lost, "width {width}");
|
|
}
|
|
// Saved and opened again.
|
|
let opened = Session::new(solidcraft_doc::Document::from_json(&s.doc.to_json()).unwrap());
|
|
assert_eq!(stored(&opened).edge_name.as_deref(), Some(name.as_str()));
|
|
assert_line(&opened, 400.0, (0.0, 50.0), "opened");
|
|
// Undo and redo of the change.
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_line(&s, 60.0, (0.0, 50.0), "undone");
|
|
run(&mut s, "edit.redo", json!({}));
|
|
assert_line(&s, 400.0, (0.0, 50.0), "redone");
|
|
// Editing the sketch keeps the edge, and stores the link's point where the edge is now.
|
|
run(&mut s, "sketch.edit", json!({"sketch": "Top"}));
|
|
run(&mut s, "sketch.point", json!({"point": [10, 10]}));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
assert_eq!(trouble(&s), "");
|
|
assert_line(&s, 400.0, (0.0, 50.0), "edited");
|
|
assert_eq!(stored(&s).edge_name.as_deref(), Some(name.as_str()));
|
|
assert!(matches!(&stored(&s).source, LinkSource::Edge { body, at } if body == "Body1" && at.dist(Vec3::new(400.0, 25.0, 20.0)) < 1e-6));
|
|
}
|
|
|
|
/// A slot cut through the plate from x = 80 to 120, y = 20 to 30 leaves the picked edge in two
|
|
/// pieces, y = 0..20 and y = 30..50. The link, picked at y = 10, gives the piece it was picked
|
|
/// on and says the edge was split; without the slot it is the whole edge again.
|
|
#[test]
|
|
fn a_projected_edge_that_was_split_gives_its_nearest_piece_and_says_so() {
|
|
let mut s = Session::default();
|
|
plate(&mut s);
|
|
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "SlotSketch"}));
|
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [80, 20], "p1": [120, 30]}));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"sketch": "SlotSketch", "distance": "20 mm", "operation": "cut", "name": "Slot"}));
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Slot", "suppressed": true}));
|
|
project_right_edge(&mut s, 10.0);
|
|
assert_line(&s, 100.0, (0.0, 50.0), "drawn");
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Slot", "suppressed": false}));
|
|
assert_line(&s, 100.0, (0.0, 20.0), "split");
|
|
assert!(trouble(&s).contains("was split"), "{}", trouble(&s));
|
|
assert!(!link(&s).lost);
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Slot", "suppressed": true}));
|
|
assert_line(&s, 100.0, (0.0, 50.0), "whole again");
|
|
assert_eq!(trouble(&s), "");
|
|
}
|
|
|
|
/// A fillet of radius 4 takes the picked edge away. Its two tangent edges are 4 from the picked
|
|
/// point, the nearest edges there are now: the link is lost and keeps its line, it takes
|
|
/// neither of them. Without the fillet the link has its edge again.
|
|
#[test]
|
|
fn a_projected_edge_that_is_gone_is_lost_not_replaced() {
|
|
let mut s = Session::default();
|
|
plate(&mut s);
|
|
run(&mut s, "solid.fillet", json!({"edges": [[100, 25, 20]], "radius": "4 mm", "name": "Round"}));
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Round", "suppressed": true}));
|
|
project_right_edge(&mut s, 25.0);
|
|
assert_line(&s, 100.0, (0.0, 50.0), "drawn");
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Round", "suppressed": false}));
|
|
let said = trouble(&s);
|
|
assert!(said.contains("projected reference lost: edge") && said.contains("no longer exists"), "{said}");
|
|
assert!(link(&s).lost);
|
|
assert_line(&s, 100.0, (0.0, 50.0), "the last geometry is kept");
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Round", "suppressed": true}));
|
|
assert_eq!(trouble(&s), "");
|
|
assert!(!link(&s).lost);
|
|
assert_line(&s, 100.0, (0.0, 50.0), "found again");
|
|
}
|
|
|
|
/// A boss 20 x 20 x 10 made as its own body on the plate and joined into it afterwards: no body
|
|
/// has the link's body name any more. The join left the boss's top edge from (30, 10, 30) to
|
|
/// (30, 30, 30) as it was, and the link finds it by its name on the body that has it now.
|
|
#[test]
|
|
fn a_projected_edge_whose_body_was_joined_into_another_is_found_there() {
|
|
let mut s = Session::default();
|
|
plate(&mut s);
|
|
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "20 mm", "name": "BossSketch"}));
|
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [10, 10], "p1": [30, 30]}));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
run(&mut s, "solid.extrude", json!({"sketch": "BossSketch", "distance": "10 mm", "operation": "new", "name": "Boss", "body_name": "Boss"}));
|
|
run(&mut s, "solid.combine", json!({"target": "Body1", "tools": ["Boss"], "operation": "join"}));
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Combine1", "suppressed": true}));
|
|
run(&mut s, "sketch.create", json!({"plane": "XY", "offset": "30 mm", "name": "Top"}));
|
|
run(&mut s, "sketch.project", json!({"refs": [{"edge": [30, 20, 30], "body": "Boss"}]}));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
assert_line(&s, 30.0, (10.0, 30.0), "drawn");
|
|
assert!(stored(&s).edge_name.is_some());
|
|
run(&mut s, "timeline.suppress", json!({"feature": "Combine1", "suppressed": false}));
|
|
assert!(s.model.state().body("Boss").is_none(), "the boss is part of the plate now");
|
|
assert_eq!(trouble(&s), "");
|
|
assert!(!link(&s).lost);
|
|
assert_line(&s, 30.0, (10.0, 30.0), "joined");
|
|
assert!(matches!(&link(&s).source, LinkSource::Edge { body, .. } if body == "Body1"));
|
|
}
|
|
|
|
/// A design saved before links kept the edge's name (format 16) has a link with a point only.
|
|
/// It opens and is found by that point, as it was, and takes the edge's name when its sketch is
|
|
/// next edited: undoing the edit takes the name away again, redoing it brings it back.
|
|
#[test]
|
|
fn an_edge_link_saved_without_a_name_is_found_as_before_and_named_by_the_next_edit() {
|
|
let mut s = Session::default();
|
|
plate(&mut s);
|
|
project_right_edge(&mut s, 25.0);
|
|
let name = edge_named_at(&mut s, [100.0, 25.0, 20.0]);
|
|
let mut saved: Value = serde_json::from_str(&s.doc.to_json()).unwrap();
|
|
let links = saved["features"].as_array_mut().unwrap().iter_mut().filter_map(|f| f.pointer_mut("/sketch/links")?.as_array_mut());
|
|
let removed = links.flatten().filter_map(|l| l.as_object_mut()?.remove("edge_name")).count();
|
|
assert_eq!(removed, 1, "the one projected edge");
|
|
saved["format"] = json!("solidcraft/16");
|
|
let mut s = Session::new(solidcraft_doc::Document::from_json(&saved.to_string()).unwrap());
|
|
assert_eq!(s.doc.format, solidcraft_doc::format::format_string(solidcraft_doc::format::FORMAT), "upgraded on opening");
|
|
assert_eq!(stored(&s).edge_name, None);
|
|
assert_eq!(trouble(&s), "");
|
|
assert_line(&s, 100.0, (0.0, 50.0), "opened");
|
|
// Found by its point: 0.2 further out the picked edge is still the nearest one.
|
|
set_width(&mut s, 100.2);
|
|
assert_eq!(stored(&s).edge_name, None);
|
|
assert_eq!(trouble(&s), "");
|
|
assert_line(&s, 100.2, (0.0, 50.0), "width 100.2");
|
|
run(&mut s, "sketch.edit", json!({"sketch": "Top"}));
|
|
run(&mut s, "sketch.point", json!({"point": [10, 10]}));
|
|
assert_eq!(stored(&s).edge_name.as_deref(), Some(name.as_str()));
|
|
run(&mut s, "edit.undo", json!({}));
|
|
assert_eq!(stored(&s).edge_name, None);
|
|
run(&mut s, "edit.redo", json!({}));
|
|
assert_eq!(stored(&s).edge_name.as_deref(), Some(name.as_str()));
|
|
run(&mut s, "sketch.finish", json!({}));
|
|
set_width(&mut s, 150.0);
|
|
assert_eq!(trouble(&s), "");
|
|
assert_line(&s, 150.0, (0.0, 50.0), "width 150");
|
|
}
|