fix: a dimension from linked geometry keeps its side when the source moves past it (#614)
A point dimensioned from linked geometry (projected, included or intersected) keeps its side when a parameter moves the link's source past it: the solve reads sides and turns from the stored state instead of from beside the moved line, both on recompute and when editing the sketch. Co-authored-by: NOWN-DEV <[email protected]>
This commit is contained in:
1 parent
bf962a9989
commit
24974e6f37
13 files changed
+392
-27
No files matched your search
@@ -294,7 +294,7 @@ pub(super) fn edit_solved<T>(
|
||||
let unsolved = sk.clone();
|
||||
#[cfg(test)]
|
||||
solve_counts::record(Analysis::Full);
|
||||
let rep = doc.solve_sketch(&vals, &mut sk, &stored)?;
|
||||
let rep = doc.solve_sketch(&vals, &mut sk, &stored, None)?;
|
||||
Some(settle(strict, rep, &mut sk, unsolved)?)
|
||||
}
|
||||
};
|
||||
@@ -336,7 +336,7 @@ pub(super) fn counted_solve(
|
||||
pub(super) fn solve_current(sk: &mut Sketch, doc: &solidcraft_doc::Document) -> Result<SolveReport> {
|
||||
let stored = sk.driving_values();
|
||||
let (values, _) = doc.param_values();
|
||||
Ok(doc.solve_sketch(&values, sk, &stored)?)
|
||||
Ok(doc.solve_sketch(&values, sk, &stored, None)?)
|
||||
}
|
||||
|
||||
/// Solves run on this thread by [`counted_solve`] and the edit's own solve: (all of them, those
|
||||
|
||||
@@ -166,8 +166,14 @@ fn owner_frame(s: &Session, doc: &Document, comp: u64) -> solidcraft_doc::Mat {
|
||||
if comp == s.active_component { s.active_component_frame() } else { doc.component_transform(comp) }
|
||||
}
|
||||
|
||||
/// Re-resolve the links of a sketch being edited against the model before it, so edits see
|
||||
/// current reference geometry. Returns warnings.
|
||||
/// Re-resolve the links of a sketch being edited (`sk`, a copy of the stored sketch `id`)
|
||||
/// against the model before it, so edits see current reference geometry. Returns warnings.
|
||||
///
|
||||
/// When a link moved, the rest of the sketch follows it as the evaluation solves it, keeping
|
||||
/// the stored sides and turns, so the edit goes on from the sketch the model shows. Left where
|
||||
/// it was stored beside a line that has moved past it, a dimensioned point would give the
|
||||
/// edit's solve the other side to keep. A sketch that does not solve that way keeps its stored
|
||||
/// geometry, with the links moved.
|
||||
pub(super) fn refresh(s: &Session, doc: &Document, id: u64, sk: &mut Sketch) -> Vec<String> {
|
||||
if sk.links.is_empty() {
|
||||
return Vec::new();
|
||||
@@ -183,10 +189,21 @@ pub(super) fn refresh(s: &Session, doc: &Document, id: u64, sk: &mut Sketch) ->
|
||||
return vec!["lost face attachment; reattach explicitly".into()];
|
||||
};
|
||||
let Ok(plane) = sketch_plane(&doc, id, &st) else { return Vec::new() };
|
||||
match attachment {
|
||||
let warnings = match attachment {
|
||||
Some(anchor) => solidcraft_doc::project::refresh_face_anchor_links(&doc, &vals, &st, &plane, sk, anchor),
|
||||
None => solidcraft_doc::project::refresh_links(&doc, &vals, &st, &plane, sk),
|
||||
};
|
||||
if let Ok(stored) = doc.sketch(id)
|
||||
&& sk.positions_in(stored).is_some()
|
||||
{
|
||||
let mut solved = sk.clone();
|
||||
let follows = solved.refresh_offsets().is_ok()
|
||||
&& doc.solve_sketch(&vals, &mut solved, &stored.driving_values(), Some(stored)).is_ok_and(|report| report.keeps_orientation());
|
||||
if follows {
|
||||
*sk = solved;
|
||||
}
|
||||
}
|
||||
warnings
|
||||
}
|
||||
|
||||
/// Add a link for `src` to sketch `id` in `doc` (not stored in the session). Returns the link id.
|
||||
@@ -547,6 +564,10 @@ pub(super) fn auto_project_face(s: &mut Session, id: u64, _at: Vec3) -> Result<O
|
||||
#[path = "sketch_project_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_project_side_tests.rs"]
|
||||
mod side_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_model_plane_tests.rs"]
|
||||
mod model_plane_tests;
|
||||
@@ -0,0 +1,180 @@
|
||||
//! A dimension from linked geometry keeps its side when the link's source moves: a point held
|
||||
//! 5 from a projected line stays on the side it was drawn on wherever a parameter takes the
|
||||
//! line, also past the place the point is stored at.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use solidcraft_geom::Vec2;
|
||||
|
||||
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}"),
|
||||
}
|
||||
}
|
||||
|
||||
/// Sketch `A`: the horizontal line `rail`, 100 long, at the height the parameter `height`
|
||||
/// (50) gives it.
|
||||
fn rail_sketch(s: &mut Session) {
|
||||
run(s, "parameters.add", json!({"name": "height", "expression": "50 mm"}));
|
||||
run(s, "sketch.create", json!({"plane": "XY", "name": "A"}));
|
||||
run(s, "sketch.line", json!({"points": [[0, 50], [100, 50]], "ids": ["rail"]}));
|
||||
run(s, "sketch.constraint.horizontal_vertical", json!({"line": "rail", "mode": "horizontal"}));
|
||||
run(s, "sketch.constraint.horizontal_vertical", json!({"points": ["origin", "rail.start"], "mode": "vertical"}));
|
||||
run(s, "sketch.dimension", json!({"entities": ["origin", "rail.start"], "value": "height"}));
|
||||
run(s, "sketch.dimension", json!({"entities": ["rail"], "value": "100 mm"}));
|
||||
run(s, "sketch.finish", json!({}));
|
||||
}
|
||||
|
||||
/// Sketch `S` (left open): `rail` brought in by `command`, and the point `p` at (50, `y`),
|
||||
/// 50 from the origin horizontally. Returns the linked line.
|
||||
fn point_by_the_rail(s: &mut Session, command: &str, y: f64) -> String {
|
||||
rail_sketch(s);
|
||||
run(s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||
let r = run(s, command, json!({"refs": [{"sketch": "A", "curve": "rail"}]}));
|
||||
let rail = r["links"][0]["curves"][0].as_str().unwrap().to_string();
|
||||
run(s, "sketch.point", json!({"point": [50, y], "id": "p"}));
|
||||
run(s, "sketch.dimension", json!({"entities": ["origin", "p"], "type": "horizontal", "value": "50 mm"}));
|
||||
rail
|
||||
}
|
||||
|
||||
/// `p` held 5 from `line`: the dimension's parameter.
|
||||
fn five_from(s: &mut Session, line: &str) -> String {
|
||||
let r = run(s, "sketch.dimension", json!({"entities": ["p", line], "type": "distance", "value": "5 mm"}));
|
||||
r["param"].as_str().unwrap().to_string()
|
||||
}
|
||||
|
||||
fn set_height(s: &mut Session, height: f64) {
|
||||
run(s, "parameters.change", json!({"name": "height", "expression": format!("{height} mm")}));
|
||||
}
|
||||
|
||||
/// Where the model has `p` of sketch `S`.
|
||||
fn p(s: &Session) -> Vec2 {
|
||||
let st = s.model.state();
|
||||
let sk = &st.sketch(s.doc.find_feature("S").unwrap().id).unwrap().sketch;
|
||||
sk.point(sk.resolve_point("p").unwrap()).unwrap()
|
||||
}
|
||||
|
||||
/// `p` is at (50, `y`), and the sketch has nothing to report.
|
||||
#[track_caller]
|
||||
fn assert_p(s: &Session, y: f64, when: &str) {
|
||||
assert!(p(s).dist(Vec2::new(50.0, y)) < 1e-9, "{when}: {:?}", p(s));
|
||||
let feature = s.model.result(s.doc.find_feature("S").unwrap().id).unwrap();
|
||||
assert_eq!((&feature.error, &feature.warning), (&None, &None), "{when}");
|
||||
}
|
||||
|
||||
/// A line of sketch `A` whose height a parameter drives, projected into sketch `S`, whose point
|
||||
/// is dimensioned 5 from the projection.
|
||||
#[test]
|
||||
fn a_distance_from_a_projected_line_keeps_its_side_when_the_source_moves_past() {
|
||||
let mut s = Session::default();
|
||||
let rail = point_by_the_rail(&mut s, "sketch.project", 45.0);
|
||||
five_from(&mut s, &rail);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
assert_p(&s, 45.0, "drawn");
|
||||
// The source drops below the point's stored place: the point stays below the projection.
|
||||
for height in [20.0, 50.0, 6.0, 400.0] {
|
||||
set_height(&mut s, height);
|
||||
assert_p(&s, height - 5.0, &format!("height {height}"));
|
||||
}
|
||||
// Editing the sketch afterwards refreshes the projection first; the side is the one from before.
|
||||
set_height(&mut s, 20.0);
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "S"}));
|
||||
run(&mut s, "sketch.point", json!({"point": [80, 80]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
assert_p(&s, 15.0, "after an edit");
|
||||
}
|
||||
|
||||
/// The same from the other side: the point above the line, the source rising past it and
|
||||
/// coming back; and with the line brought in by Include 3D Geometry.
|
||||
#[test]
|
||||
fn a_point_above_the_line_stays_above_when_the_source_rises_past_and_returns() {
|
||||
for command in ["sketch.project", "sketch.include_3d"] {
|
||||
let mut s = Session::default();
|
||||
let rail = point_by_the_rail(&mut s, command, 55.0);
|
||||
five_from(&mut s, &rail);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
for height in [80.0, 400.0, 50.0, 6.0, 50.0] {
|
||||
set_height(&mut s, height);
|
||||
assert_p(&s, height + 5.0, &format!("{command}, height {height}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Model geometry in a sketch on XZ (whose y runs down the model's Z): the top of a box whose
|
||||
/// height a parameter drives, as a projected edge, as an edge of a projected face and as a
|
||||
/// section of the body. The point is 5 below the top, inside the box.
|
||||
#[test]
|
||||
fn a_distance_from_model_geometry_keeps_its_side_when_the_model_moves_past() {
|
||||
// A projected edge is found again by the point picked on it, so it follows small steps
|
||||
// only; a face (by its name) and a body follow any.
|
||||
let small: &[f64] = &[42.0, 50.0, 58.0, 50.0, 44.0];
|
||||
let large: &[f64] = &[20.0, 50.0, 6.0, 400.0];
|
||||
let cases = [
|
||||
("sketch.project", json!({"edge": [50, 0, 50]}), 0.0, small),
|
||||
("sketch.project", json!({"face": [50, 0, 25]}), 0.0, large),
|
||||
("sketch.intersect", json!({"body": "Body1"}), -20.0, large),
|
||||
];
|
||||
for (command, link, front, heights) in cases {
|
||||
let mut s = Session::default();
|
||||
run(&mut s, "parameters.add", json!({"name": "height", "expression": "50 mm"}));
|
||||
run(&mut s, "solid.box", json!({"corner": [0, front, 0], "length": 100, "width": 40, "height": "height"}));
|
||||
run(&mut s, "sketch.create", json!({"plane": "XZ", "name": "S"}));
|
||||
run(&mut s, command, json!({"refs": [link]}));
|
||||
let curves = run(&mut s, "sketch.inspect", json!({}))["curves"].clone();
|
||||
let high = |c: &Value, end: &str| (c[end][1].as_f64().unwrap() + 50.0).abs() < 1e-6;
|
||||
let top = curves.as_array().unwrap().iter().find(|c| high(c, "start_at") && high(c, "end_at")).unwrap()["id"].as_str().unwrap().to_string();
|
||||
run(&mut s, "sketch.point", json!({"point": [50, -45], "id": "p"}));
|
||||
run(&mut s, "sketch.dimension", json!({"entities": ["origin", "p"], "type": "horizontal", "value": "50 mm"}));
|
||||
five_from(&mut s, &top);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
// (An edge found again by its picked point says so in a warning.)
|
||||
for height in heights {
|
||||
set_height(&mut s, *height);
|
||||
assert!(p(&s).dist(Vec2::new(50.0, 5.0 - height)) < 1e-9, "{command} {link}, height {height}: {:?}", p(&s));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Another projection added after the source moved stores the sketch without solving it: the
|
||||
/// sketch it stores is the one the model shows.
|
||||
#[test]
|
||||
fn a_projection_added_after_the_source_moved_leaves_the_side() {
|
||||
let mut s = Session::default();
|
||||
let rail = point_by_the_rail(&mut s, "sketch.project", 45.0);
|
||||
five_from(&mut s, &rail);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
set_height(&mut s, 20.0);
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "S"}));
|
||||
run(&mut s, "sketch.project", json!({"refs": ["origin"]}));
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
assert_p(&s, 15.0, "after the projection");
|
||||
set_height(&mut s, 50.0);
|
||||
assert_p(&s, 45.0, "back at 50");
|
||||
}
|
||||
|
||||
/// The side is still the user's to choose, by where the point is when the dimension is added:
|
||||
/// also when the source has moved past the point since it was drawn. That side is kept from
|
||||
/// then on, and through an edit of the dimension's value.
|
||||
#[test]
|
||||
fn a_dimension_added_after_the_source_moved_takes_the_side_its_point_is_on() {
|
||||
let mut s = Session::default();
|
||||
let rail = point_by_the_rail(&mut s, "sketch.project", 45.0);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
// Drawn below the line; the line drops below it.
|
||||
set_height(&mut s, 20.0);
|
||||
assert_p(&s, 45.0, "no distance yet");
|
||||
run(&mut s, "sketch.edit", json!({"sketch": "S"}));
|
||||
let distance = five_from(&mut s, &rail);
|
||||
run(&mut s, "sketch.finish", json!({}));
|
||||
assert_p(&s, 25.0, "dimensioned above the line");
|
||||
for height in [50.0, 6.0, 400.0, 20.0] {
|
||||
set_height(&mut s, height);
|
||||
assert_p(&s, height + 5.0, &format!("height {height}"));
|
||||
}
|
||||
run(&mut s, "parameters.change", json!({"name": distance, "expression": "8 mm"}));
|
||||
assert_p(&s, 28.0, "8 from the line");
|
||||
set_height(&mut s, 6.0);
|
||||
assert_p(&s, 14.0, "8 from the line, height 6");
|
||||
}
|
||||
Reference in new issue
Block a user