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:
NOWN-DEV authored and GitHub committed 2026-10-13 05:15:15 -04:00
1 parent bf962a9989
commit 24974e6f37
13 files changed
+392 -27

No files matched your search

+9 -2
View File
@@ -2361,18 +2361,25 @@ impl Document {
/// the sketch over is refused. Offsets whose sources moved are refreshed and solved again:
/// from the stored state again after a refused or failed first solve (the geometry is still
/// the stored one), from the solved one otherwise.
///
/// `moved_from`: the stored sketch, when `sk` is that sketch with its links refreshed. A
/// link's points are where their source is now; the sides and turns the solve keeps are
/// those of the stored state ([`Sketch::positions_in`]), not the ones read beside a line
/// that has moved past a point.
pub fn solve_sketch(
&self,
vals: &std::collections::BTreeMap<String, expr::Value>,
sk: &mut Sketch,
stored: &solidcraft_sketch::DrivingValues,
moved_from: Option<&Sketch>,
) -> Result<solidcraft_sketch::SolveReport> {
self.apply_dimension_values(vals, sk)?;
let mut report = solidcraft_sketch::solve_after_value_edit(sk, stored);
let was = |sk: &Sketch| moved_from.and_then(|from| sk.positions_in(from));
let mut report = solidcraft_sketch::solve_after_edit(sk, stored, was(sk).as_deref());
if !sk.offsets.is_empty() || !sk.freeform_offsets.is_empty() {
// Source dimensions may have moved curves during the solve.
sk.refresh_offsets()?;
report = if report.ok() { solidcraft_sketch::solve(sk) } else { solidcraft_sketch::solve_after_value_edit(sk, stored) };
report = if report.ok() { solidcraft_sketch::solve(sk) } else { solidcraft_sketch::solve_after_edit(sk, stored, was(sk).as_deref()) };
}
Ok(report)
}
+3 -2
View File
@@ -3347,9 +3347,10 @@ fn eval_feature(doc: &Document, vals: &BTreeMap<String, Value>, f: &Feature, st:
if let Some(w) = face_warning {
warns.insert(0, w);
}
// The stored sketch is solved for its stored values.
// The stored sketch is solved for its stored values, with its links where they
// were stored: a link that moved must not change the side a dimension from it is on.
let stored = sk.driving_values();
let report = doc.solve_sketch(vals, &mut sk, &stored)?;
let report = doc.solve_sketch(vals, &mut sk, &stored, Some(sketch))?;
if !report.keeps_orientation() {
warns.insert(0, report.failure(|f| format!("the sketch constraints conflict ({f})")));
}
+2 -2
View File
@@ -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
+24 -3
View File
@@ -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");
}
+7 -8
View File
@@ -94,11 +94,13 @@ impl Placement {
/// Place the points the constraints pin down; `given[i]` marks the points that are constants
/// for this solve. Branches are chosen to match the orientation of `reference` (positions
/// before an interactive move) where it covers a point, of the sketch's current positions
/// otherwise; a reference longer than the point list is ignored. `on_curve` holds the sketch's
/// point-on-curve pairs ([`crate::solver::points_on_curves`]); the decomposition works within
/// what is left of `budget`, and charges it.
pub(crate) fn seed(sk: &Sketch, given: &[bool], reference: Option<&[Vec2]>, on_curve: &[(usize, usize)], budget: &mut Budget) -> Seed {
/// before an interactive move, or the stored ones after linked geometry moved) where it covers
/// a point, of the sketch's current positions otherwise; a reference longer than the point list
/// is ignored. `edits`: the solve applies edits; a drag step solves against its reference and
/// moves only held points. `on_curve` holds the sketch's point-on-curve pairs
/// ([`crate::solver::points_on_curves`]); the decomposition works within what is left of
/// `budget`, and charges it.
pub(crate) fn seed(sk: &Sketch, given: &[bool], reference: Option<&[Vec2]>, edits: bool, on_curve: &[(usize, usize)], budget: &mut Budget) -> Seed {
let n = sk.points.len();
let reference = reference.filter(|r| r.len() <= n);
let before = |i: usize| match reference.and_then(|r| r.get(i)) {
@@ -139,9 +141,6 @@ pub(crate) fn seed(sk: &Sketch, given: &[bool], reference: Option<&[Vec2]>, on_c
if !switch::decompose_on() {
return out;
}
// A drag step solves against its reference and moves only held points; any other solve
// applies edits.
let edits = reference.is_none();
let mut clusters = Decomposition::new(&reading, &prop.anchors, edits, budget.left());
if !clusters.built() {
return out;
+4 -4
View File
@@ -693,7 +693,7 @@ fn hostile_lines_and_rounds_neither_panic_nor_change_the_report() {
let mut c = base();
case(&mut c.sk);
let sk = edited(&c);
let s = seed(&sk, &given(&sk), Some(&[v(f64::NAN, 0.0)]), &points_on_curves(&sk), &mut Budget::full());
let s = seed(&sk, &given(&sk), Some(&[v(f64::NAN, 0.0)]), false, &points_on_curves(&sk), &mut Budget::full());
assert!(s.pos.iter().flatten().all(|p| p.is_finite()), "case {i}: {s:?}");
assert!(s.radius.iter().flatten().all(|r| r.is_finite() && *r > 0.0), "case {i}: {s:?}");
let mut t = c.sk.clone();
@@ -751,7 +751,7 @@ fn a_dimensioned_radius_is_seeded_with_nothing_given() {
let mut sk = Sketch::new();
let circle = sk.add_circle(v(10.0, 5.0), 10.0, None, None).unwrap();
push(&mut sk, ConstraintKind::Diameter { c: circle, value: 30.0 });
let s = seed(&sk, &vec![false; sk.points.len()], None, &[], &mut Budget::full());
let s = seed(&sk, &vec![false; sk.points.len()], None, true, &[], &mut Budget::full());
assert_eq!(s.radius[circle], Some(15.0));
}
@@ -773,10 +773,10 @@ fn the_touch_point_comes_from_the_point_on_curve_index() {
push(&mut sk, ConstraintKind::Tangent { a: l, b: circle });
push(&mut sk, ConstraintKind::PointOnCurve { p: b, c: circle });
let on_curve = crate::solver::points_on_curves(&sk);
let with = seed(&sk, &given(&sk), None, &on_curve, &mut Budget::full());
let with = seed(&sk, &given(&sk), None, true, &on_curve, &mut Budget::full());
assert!(with.placed_by[c].is_some_and(Placement::anchors), "{with:?}");
assert!(with.pos[c].unwrap().dist(v(40.0, 12.0) + d.perp() * 8.0) < 1e-9, "{with:?}");
let without = seed(&sk, &given(&sk), None, &[], &mut Budget::full());
let without = seed(&sk, &given(&sk), None, true, &[], &mut Budget::full());
assert!(!without.placed_by[c].is_some_and(Placement::anchors), "{without:?}");
}
+2 -2
View File
@@ -528,7 +528,7 @@ fn hostile_input_neither_panics_nor_changes_the_report() {
case(&mut sk, a, b, l);
let mut mask = given(&sk);
mask.push(true);
let s = seed(&sk, &mask, Some(&[v(f64::NAN, 0.0)]), &points_on_curves(&sk), &mut Budget::full());
let s = seed(&sk, &mask, Some(&[v(f64::NAN, 0.0)]), false, &points_on_curves(&sk), &mut Budget::full());
assert!(s.pos.iter().flatten().all(|p| p.is_finite()), "case {i}: {s:?}");
let n = sk.points.len();
for reference in [None, Some(vec![Vec2::ZERO; 1]), Some(vec![v(f64::NAN, f64::INFINITY); n]), Some(vec![Vec2::ZERO; n + 3])] {
@@ -902,7 +902,7 @@ fn given_points_are_never_placed() {
push(&mut sk, ConstraintKind::Distance { p: a, q: d, value: 36.0 });
let mask = given(&sk);
assert!(mask[g]);
let out = seed(&sk, &mask, None, &points_on_curves(&sk), &mut Budget::full());
let out = seed(&sk, &mask, None, true, &points_on_curves(&sk), &mut Budget::full());
assert_eq!((out.pos[g], out.placed_by[g]), (None, None), "{out:?}");
assert_eq!(out.placed_by[d], None, "nothing is built on it: {out:?}");
}
+2 -1
View File
@@ -55,7 +55,8 @@ pub use profiles::{
};
pub use shape::{Shape, intersections};
pub use solver::{
Analysis, DrivingValues, Hold, SolveReport, SolveStatus, measure_dimension, solve, solve_after_value_edit, solve_holding, solve_with,
Analysis, DrivingValues, Hold, SolveReport, SolveStatus, measure_dimension, solve, solve_after_edit, solve_after_value_edit, solve_holding,
solve_with,
};
pub use text::{MAX_TEXT_CHARS, text_geometry, text_geometry_in};
pub use wire::{MAX_FIT_POINTS, MAX_WIRE_POINTS, Wire, WireSpace, fit_polyline};
+19
View File
@@ -852,6 +852,21 @@ impl Sketch {
Ok(true)
}
/// Where this sketch's points were in `stored`, the same sketch before its links were
/// moved to where their sources are now ([`Sketch::update_link`]): point by point, as
/// [`crate::solve_after_edit`] takes them. Points are matched by id (a rebuilt link has
/// new ones); a point `stored` does not have is where it is now. `None` when none moved.
pub fn positions_in(&self, stored: &Sketch) -> Option<Vec<Vec2>> {
let aligned = self.points.len() == stored.points.len() && self.points.iter().zip(&stored.points).all(|(p, q)| p.id == q.id);
let was: Vec<Vec2> = if aligned {
stored.points.iter().map(|p| p.pos).collect()
} else {
let by_id: std::collections::BTreeMap<&str, Vec2> = stored.points.iter().map(|p| (p.id.as_str(), p.pos)).collect();
self.points.iter().map(|p| by_id.get(p.id.as_str()).copied().unwrap_or(p.pos)).collect()
};
self.points.iter().zip(&was).any(|(p, w)| p.pos != *w).then_some(was)
}
fn drop_link_entities(&mut self, id: &str) {
let curves = self.link_curves(id);
let keep = self.links.clone();
@@ -921,3 +936,7 @@ impl Sketch {
#[cfg(test)]
#[path = "link_identity_tests.rs"]
mod identity_tests;
#[cfg(test)]
#[path = "link_side_tests.rs"]
mod side_tests;
+122
View File
@@ -0,0 +1,122 @@
//! A dimension from a link keeps its side when the link moves: the solve after the links are
//! refreshed reads sides and turns where the points were stored ([`Sketch::positions_in`]),
//! not beside a line that has already moved past them.
use super::*;
use crate::{ConstraintKind, solve, solve_after_edit};
fn source() -> LinkSource {
LinkSource::SketchCurve { sketch: 1, curve: "rail".into() }
}
fn rail(height: f64) -> Vec<LinkGeom> {
vec![LinkGeom::Line(Vec2::new(0.0, height), Vec2::new(80.0, height))]
}
/// What the evaluation does with a stored sketch whose link's source moved: the link goes
/// where the source is, then the sketch is solved from its stored state. `kept`: the solve is
/// given the stored positions.
fn moved(stored: &Sketch, link: &str, geom: &[LinkGeom], kept: bool) -> Sketch {
let mut sk = stored.clone();
assert!(!sk.update_link(link, geom).unwrap(), "the link keeps its points");
let before = if kept { sk.positions_in(stored) } else { None };
assert!(solve_after_edit(&mut sk, &stored.driving_values(), before.as_deref()).keeps_orientation());
sk
}
fn near(at: Vec2, x: f64, y: f64) -> bool {
at.dist(Vec2::new(x, y)) < 1e-9
}
#[test]
fn a_point_dimensioned_from_a_linked_line_keeps_its_side_when_the_line_moves_past() {
let mut sk = Sketch::new();
let link = sk.add_link(LinkKind::Project, source(), &rail(50.0)).unwrap();
let l = sk.link_curves(&link)[0];
let p = sk.add_point(Vec2::new(40.0, 45.0), None).unwrap();
sk.add_constraint(ConstraintKind::DistanceX { p: 0, q: p, value: 40.0 }, None).unwrap();
sk.add_constraint(ConstraintKind::PointLineDistance { p, l, value: 5.0 }, None).unwrap();
assert!(solve(&mut sk).ok());
// Past the point (20, 6, 44), away from it (400) and not as far as it (48).
for height in [20.0, 6.0, 44.0, 400.0, 48.0] {
let at = moved(&sk, &link, &rail(height), true).points[p].pos;
assert!(near(at, 40.0, height - 5.0), "{height}: {at:?}");
}
// Read where the line is now, the stored point is above it: the solution on that side.
let at = moved(&sk, &link, &rail(20.0), false).points[p].pos;
assert!(near(at, 40.0, 25.0), "{at:?}");
}
#[test]
fn a_dimension_along_an_axis_keeps_its_side_of_a_linked_point_that_moves_past() {
for upright in [true, false] {
let end = |h: f64| vec![LinkGeom::Point(if upright { Vec2::new(80.0, h) } else { Vec2::new(h, 80.0) })];
let mut sk = Sketch::new();
let link = sk.add_link(LinkKind::Project, LinkSource::SketchPoint { sketch: 1, point: "end".into() }, &end(50.0)).unwrap();
let q = sk.link_points(&link)[0];
let p = sk.add_point(if upright { Vec2::new(40.0, 45.0) } else { Vec2::new(45.0, 40.0) }, None).unwrap();
if upright {
sk.add_constraint(ConstraintKind::DistanceX { p: 0, q: p, value: 40.0 }, None).unwrap();
sk.add_constraint(ConstraintKind::DistanceY { p: q, q: p, value: 5.0 }, None).unwrap();
} else {
sk.add_constraint(ConstraintKind::DistanceY { p: 0, q: p, value: 40.0 }, None).unwrap();
sk.add_constraint(ConstraintKind::DistanceX { p: q, q: p, value: 5.0 }, None).unwrap();
}
assert!(solve(&mut sk).ok());
for height in [20.0, 6.0, 400.0] {
let at = moved(&sk, &link, &end(height), true).points[p].pos;
let (along, across) = if upright { (at.y, at.x) } else { (at.x, at.y) };
assert!((along - (height - 5.0)).abs() < 1e-9 && (across - 40.0).abs() < 1e-9, "upright {upright}, {height}: {at:?}");
}
let at = moved(&sk, &link, &end(20.0), false).points[p].pos;
assert!(((if upright { at.y } else { at.x }) - 25.0).abs() < 1e-9, "upright {upright}: {at:?}");
}
}
/// A corner 50 from both ends of a linked line: below the line, or its mirror image above.
#[test]
fn a_corner_dimensioned_from_two_linked_points_keeps_its_turn_when_they_move_past() {
let mut sk = Sketch::new();
let link = sk.add_link(LinkKind::Project, source(), &rail(50.0)).unwrap();
let ends = sk.link_points(&link);
let p = sk.add_point(Vec2::new(40.0, 20.0), None).unwrap();
for end in ends {
sk.add_constraint(ConstraintKind::Distance { p: end, q: p, value: 50.0 }, None).unwrap();
}
assert!(solve(&mut sk).ok());
assert!(near(sk.points[p].pos, 40.0, 20.0));
for height in [10.0, -200.0, 300.0] {
let at = moved(&sk, &link, &rail(height), true).points[p].pos;
assert!(near(at, 40.0, height - 30.0), "{height}: {at:?}");
}
let at = moved(&sk, &link, &rail(10.0), false).points[p].pos;
assert!(near(at, 40.0, 40.0), "{at:?}");
}
#[test]
fn stored_positions_are_matched_by_point_id() {
let mut stored = Sketch::new();
let link = stored.add_link(LinkKind::Project, source(), &rail(50.0)).unwrap();
let p = stored.add_point(Vec2::new(40.0, 45.0), Some("p")).unwrap();
assert_eq!(stored.positions_in(&stored), None, "nothing moved");
let mut sk = stored.clone();
sk.update_link(&link, &rail(20.0)).unwrap();
let was: Vec<Vec2> = stored.points.iter().map(|q| q.pos).collect();
assert_eq!(sk.positions_in(&stored), Some(was));
// A link of another shape is built anew: its points are new, and the points after it
// change places in the list.
let mut sk = stored.clone();
let bent = [LinkGeom::Line(Vec2::new(0.0, 20.0), Vec2::new(40.0, 30.0)), LinkGeom::Line(Vec2::new(40.0, 30.0), Vec2::new(80.0, 20.0))];
assert!(sk.update_link(&link, &bent).unwrap());
let now = sk.point_index("p").unwrap();
assert_ne!(now, p);
assert_eq!(sk.positions_in(&stored), None, "the new points have no stored place and `p` has not moved");
sk.points[now].pos = Vec2::new(60.0, 45.0);
let was = sk.positions_in(&stored).unwrap();
assert_eq!(was[now], Vec2::new(40.0, 45.0));
for i in sk.link_points(&link) {
assert_eq!(was[i], sk.points[i].pos);
}
}
+17 -2
View File
@@ -1275,8 +1275,19 @@ pub type DrivingValues = std::collections::BTreeMap<String, f64>;
/// again by stepping its changed values from old to new (see `continue_values`). Where no
/// value changed a solve behaves as [`solve`].
pub fn solve_after_value_edit(sk: &mut Sketch, old: &DrivingValues) -> SolveReport {
solve_after_edit(sk, old, None)
}
/// [`solve_after_value_edit`] of a sketch whose linked geometry may have moved as well: once
/// its links are refreshed ([`Sketch::update_link`]), the linked points are where their sources
/// are now and everything else is still the stored solved state. `stored` has every point's
/// position in that state ([`Sketch::positions_in`]; `None` when no link moved), and the sides
/// and turns the solve keeps are read there, as [`solve_with`] reads them from the positions
/// before an interactive move. Read where the links are now, a point dimensioned from a
/// projected line is on the far side of it as soon as the source moves the line past the point.
pub fn solve_after_edit(sk: &mut Sketch, old: &DrivingValues, stored: Option<&[Vec2]>) -> SolveReport {
let old_values: Vec<Option<f64>> = sk.constraints.iter().map(|c| old.get(&c.id).copied()).collect();
solve_from(sk, &Hold::default(), None, Analysis::Full, Some(&old_values), &mut Budget::full())
solve_from(sk, &Hold::default(), stored, Analysis::Full, Some(&old_values), &mut Budget::full())
}
impl Sketch {
@@ -1736,7 +1747,11 @@ fn solve_inner(
let mut constructed = vec![false; lay.n];
if crate::construct::switch::seed_on() {
let given: Vec<bool> = lay.pvar.iter().map(Option::is_none).collect();
let seeded = crate::construct::seed(sk, &given, reference, &on_curve, budget);
// A drag step solves against its reference and moves only held points. Any other solve
// applies edits: also the one after linked geometry moved, whose reference is the
// stored state.
let edits = reference.is_none() || old_values.is_some();
let seeded = crate::construct::seed(sk, &given, reference, edits, &on_curve, budget);
for (i, (at, how)) in seeded.pos.iter().zip(&seeded.placed_by).enumerate() {
if let (Some(at), Some(how), Some(v)) = (at, how, lay.pvar.get(i).copied().flatten())
&& how.anchors()
+1 -1
View File
@@ -39,7 +39,7 @@ pub(crate) fn given(sk: &Sketch) -> Vec<bool> {
/// The constructive seed of a plain solve.
pub(crate) fn seed_of(sk: &Sketch) -> crate::construct::Seed {
crate::construct::seed(sk, &given(sk), None, &crate::solver::points_on_curves(sk), &mut crate::decompose::Budget::full())
crate::construct::seed(sk, &given(sk), None, true, &crate::solver::points_on_curves(sk), &mut crate::decompose::Budget::full())
}
/// Rough rectangle from four lines sharing corner points.