mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-08 13:01:52 +00:00
* claim: #12092 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sketch Tangent: a line's end point and a curve make a tangent joint (#12092) Tangent took two curves only and refused a picked end point with "unknown sketch curve l1.end". It now takes a line's or an arc's end point with a curve: the end lies on the curve (point on curve) and the two curves are tangent there, in one undo step. Two end points make the ends coincident and the curves tangent. Only the picked end moves, so the line turns about its other end and gets longer or shorter; the other curve stays. Parallel, Perpendicular, Equal and Collinear take no point, so a click on a line's end point now picks the line for them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sketch Tangent: at a joint, add only the tangent (#12092) When the picked end already lies on the other curve (it is that curve's end, or a coincident joins them), Tangent added a redundant point-on-curve, and a coincident given in the other order came back as the existing constraint but was reported as new. Now such a constraint counts as held and is not added. The end can't leave the other curve at a joint, so the curve it ends stays and the other curve moves. Raised in review by CodeRabbit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Sketch Tangent: x and y name the axes before a point does (#12092) Tangent looks for an end point only when an operand names no curve and no sketch axis. It checked for an existing axis line, so before that line was created a point called x or y took the axis's place. Now x and y always name the axes, as for the other constraints that take a line; a curve with that id still wins. Raised in review by CodeRabbit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ use crate::params::{bad, bool_, expr, num, req_expr, req_vec2, str_, string_list
|
||||
use crate::schema::sketch::{
|
||||
CancelResult, CoincidentMade, CoincidentParams, CoincidentResult, CollinearParams, ConcentricParams, Constrained, ConstraintResult, EditParams,
|
||||
EditResult, EqualMade, EqualParams, EqualResult, ExitParams, FinishResult, FixOutcome, FixParams, FixResult, HorizontalVerticalParams,
|
||||
MidpointParams, ParallelParams, PerpendicularParams, SymmetryParams, SymmetryResult, TangentParams,
|
||||
MidpointParams, ParallelParams, PerpendicularParams, SymmetryParams, SymmetryResult, TangentMade, TangentParams, TangentResult,
|
||||
};
|
||||
use crate::schema::sketch_shapes::{
|
||||
ArcCenterPointParams, ArcThreePointParams, CircleCenterParams, CircleThreePointParams, CircleTwoPointParams, Created, DrawResult, LineParams,
|
||||
@@ -198,8 +198,8 @@ pub static COMMANDS: &[CommandSpec] = &[
|
||||
.icon("c_tangent")
|
||||
.enabled(in_sketch)
|
||||
.contract(&TANGENT_CONTRACT)
|
||||
.output(output_schema_for::<ConstraintResult>)
|
||||
.params(concat!("a, b: curves or sketch axes (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created)", model_operand_doc!())),
|
||||
.output(output_schema_for::<TangentResult>)
|
||||
.params(concat!("a, b: curves or sketch axes (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created), or the end point of a line or an arc: that end then lies on the other curve (or on the other end point), and its curve is tangent there, in one step (`constraints` lists both)", model_operand_doc!())),
|
||||
CommandSpec::new("sketch.constraint.equal", "Equal", crate::checked!("sketch.constraint.equal", c_equal))
|
||||
.at("SKETCH", "CONSTRAINTS")
|
||||
.icon("c_equal")
|
||||
@@ -2646,12 +2646,123 @@ fn two_curves_or_axes(sk: &mut Sketch, p: &Value, cmd: &str) -> Result<(usize, u
|
||||
}
|
||||
|
||||
fn c_tangent(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let p = &super::sketch_project::project_operands(s, p, "sketch.constraint.tangent", &["a", "b"])?;
|
||||
constrain_reply(s, p, "sketch.constraint.tangent", |sk| {
|
||||
two_curves_or_axes(sk, p, "sketch.constraint.tangent")
|
||||
.and_then(|ab| two_different("sketch.constraint.tangent", ab))
|
||||
.map(|(a, b)| ConstraintKind::Tangent { a, b })
|
||||
})
|
||||
let cmd = "sketch.constraint.tangent";
|
||||
let p = &super::sketch_project::project_operands(s, p, cmd, &["a", "b"])?;
|
||||
let sk = sketch_store::read(&s.doc, sketch_store::target(s, p, cmd)?)?;
|
||||
if tangent_end(sk, p, "a").is_some() || tangent_end(sk, p, "b").is_some() {
|
||||
return tangent_at_end(s, p, cmd);
|
||||
}
|
||||
let made = constrain(s, p, cmd, |sk| {
|
||||
two_curves_or_axes(sk, p, cmd).and_then(|ab| two_different(cmd, ab)).map(|(a, b)| ConstraintKind::Tangent { a, b })
|
||||
})?;
|
||||
reply(cmd, TangentResult { made: TangentMade::Curves(made), param_warnings: Vec::new() })
|
||||
}
|
||||
|
||||
/// The point a Tangent operand names when it names no curve and no sketch axis (#12092). `x` and
|
||||
/// `y` name the axes even before their lines exist, as in [`line_or_axis_ref`].
|
||||
fn tangent_end(sk: &Sketch, p: &Value, key: &str) -> Option<usize> {
|
||||
let id = p.get(key)?.as_str()?;
|
||||
if sk.curve_index(id).is_some() || solidcraft_sketch::sketch_axis_geom(id).is_some() {
|
||||
return None;
|
||||
}
|
||||
sk.resolve_point(id)
|
||||
}
|
||||
|
||||
/// The one curve that ends at point `q`, leaving out `other`; `None` when no curve or more than
|
||||
/// one does.
|
||||
fn ended_by(sk: &Sketch, q: usize, other: Option<usize>) -> Option<usize> {
|
||||
let mut owners = sk
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, c)| Some(*i) != other && c.kind.ends().is_some_and(|(start, end)| start == q || end == q))
|
||||
.map(|(i, _)| i);
|
||||
let first = owners.next()?;
|
||||
owners.next().is_none().then_some(first)
|
||||
}
|
||||
|
||||
/// Whether `k` already holds through the sketch's own constraints, so Tangent need not add it
|
||||
/// (#12092): the same constraint (a coincident in either order), or a point on a curve that is one
|
||||
/// of that curve's ends or is joined to one by a coincident.
|
||||
fn already_held(sk: &Sketch, k: &ConstraintKind) -> bool {
|
||||
let joined = |p: usize, q: usize| {
|
||||
p == q
|
||||
|| sk
|
||||
.constraints
|
||||
.iter()
|
||||
.any(|c| !c.driven && matches!(c.kind, ConstraintKind::Coincident { p: x, q: y } if (x, y) == (p, q) || (x, y) == (q, p)))
|
||||
};
|
||||
match *k {
|
||||
ConstraintKind::Coincident { p, q } => joined(p, q),
|
||||
ConstraintKind::PointOnCurve { p, c } => {
|
||||
sk.curves.get(c).and_then(|curve| curve.kind.ends()).is_some_and(|(start, end)| joined(p, start) || joined(p, end))
|
||||
|| sk.constraints.iter().any(|x| !x.driven && x.kind == *k)
|
||||
}
|
||||
_ => sk.constraints.iter().any(|c| !c.driven && c.kind == *k),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tangent with a curve's end point for `a` or `b` (#12092): that end lies on the other curve,
|
||||
/// or on the other end point, and the curve it ends is tangent to the other curve there. Both
|
||||
/// constraints come in one step. A line's end picked with a curve is what moves; otherwise the
|
||||
/// first pick stays and the second moves, as with two curves.
|
||||
fn tangent_at_end(s: &mut Session, p: &Value, cmd: &str) -> Result<Value> {
|
||||
let not_an_end = |key: &str| {
|
||||
let id = p.get(key).and_then(Value::as_str).unwrap_or_default();
|
||||
bad(cmd, format!("`{id}` is not the end of one curve: pick a curve, or the end point of a line or an arc"))
|
||||
};
|
||||
let (out, info) = edit(s, p, cmd, true, |sk, _| {
|
||||
let rep = solve(&mut sk.clone());
|
||||
let (pa, pb) = (tangent_end(sk, p, "a"), tangent_end(sk, p, "b"));
|
||||
let ca = if pa.is_none() { Some(line_or_axis_ref(sk, p.get("a"), cmd, "a")?) } else { None };
|
||||
let cb = if pb.is_none() { Some(line_or_axis_ref(sk, p.get("b"), cmd, "b")?) } else { None };
|
||||
let a = match (ca, pa) {
|
||||
(Some(c), _) => c,
|
||||
(None, Some(q)) => ended_by(sk, q, cb).ok_or_else(|| not_an_end("a"))?,
|
||||
(None, None) => return Err(not_an_end("a")),
|
||||
};
|
||||
let b = match (cb, pb) {
|
||||
(Some(c), _) => c,
|
||||
(None, Some(q)) => ended_by(sk, q, Some(a)).ok_or_else(|| not_an_end("b"))?,
|
||||
(None, None) => return Err(not_an_end("b")),
|
||||
};
|
||||
let (a, b) = two_different(cmd, (a, b))?;
|
||||
let touch = match (pa, pb) {
|
||||
(Some(q), Some(r)) if q != r => Some(ConstraintKind::Coincident { p: q, q: r }),
|
||||
(Some(q), None) => Some(ConstraintKind::PointOnCurve { p: q, c: b }),
|
||||
(None, Some(r)) => Some(ConstraintKind::PointOnCurve { p: r, c: a }),
|
||||
_ => None,
|
||||
};
|
||||
// A joint: the end already lies on the other curve (it is that curve's end, or joined to it).
|
||||
let joint = touch.as_ref().is_some_and(|k| already_held(sk, k));
|
||||
let mut ids = Vec::new();
|
||||
if let Some(k) = touch.filter(|_| !joint) {
|
||||
ids.push(add_c(sk, k)?);
|
||||
}
|
||||
let tangent = add_c(sk, ConstraintKind::Tangent { a, b })?;
|
||||
ids.push(tangent.clone());
|
||||
// A line's end and a curve, in either order: only that end moves, so the line turns about
|
||||
// its other end and gets longer or shorter to meet the curve (#12092). At a joint that end
|
||||
// can't leave the other curve: the curve it ends stays, and the other curve moves. Otherwise,
|
||||
// as for two curves, the first pick stays and the second moves.
|
||||
let end = match (pa, pb) {
|
||||
(Some(q), None) => Some((q, a, b)),
|
||||
(None, Some(r)) => Some((r, b, a)),
|
||||
_ => None,
|
||||
};
|
||||
let far = end.filter(|_| !joint).and_then(|(q, line, other)| match sk.curves.get(line).map(|c| &c.kind) {
|
||||
Some(k @ CurveKind::Line { .. }) => k.ends().map(|(start, end)| (q, if start == q { end } else { start }, other)),
|
||||
_ => None,
|
||||
});
|
||||
let (stay, moves) = end.filter(|_| joint).map_or((a, b), |(_, own, other)| (own, other));
|
||||
match far {
|
||||
Some((q, f, other)) => apply_in_order(sk, &rep, &[Ent::Curve(other), Ent::Point(f)], Ent::Point(q), cmd, &ids)?,
|
||||
None => apply_in_order(sk, &rep, &[Ent::Curve(stay)], Ent::Curve(moves), cmd, &ids)?,
|
||||
}
|
||||
Ok((tangent, ids))
|
||||
})?;
|
||||
let (constraint, constraints) = out;
|
||||
reply(cmd, TangentResult { made: TangentMade::End { constraint, constraints, sketch: info }, param_warnings: Vec::new() })
|
||||
}
|
||||
fn c_equal(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
if p.get("curves").is_some() {
|
||||
|
||||
@@ -95,6 +95,23 @@ fn a_real_curve_named_like_an_axis_wins() {
|
||||
assert!((start.x + end.x - 10.0).abs() < 1e-7, "mirrored about x = 5: {start:?} {end:?}");
|
||||
}
|
||||
|
||||
/// Tangent also takes a curve's end point (#12092), but `x` and `y` still name the axes before
|
||||
/// their lines exist: a point called `x` does not take the axis's place.
|
||||
#[test]
|
||||
fn a_point_named_like_an_axis_leaves_tangent_on_the_axis() {
|
||||
for (a, b) in [("c", "x"), ("x", "c")] {
|
||||
let mut s = on(json!("XY"));
|
||||
run(&mut s, "sketch.point", json!({"point": [3, 7], "id": "x", "connect": false}));
|
||||
run(&mut s, "sketch.circle.center", json!({"center": [10, 4], "radius": 2, "id": "c"}));
|
||||
run(&mut s, "sketch.constraint.tangent", json!({"a": a, "b": b}));
|
||||
let sk = sketch(&s);
|
||||
assert_eq!(axis_lines(&sk).iter().map(|(n, _)| n.as_str()).collect::<Vec<_>>(), ["x"], "{a}, {b}: the x axis's line");
|
||||
let CurveKind::Circle { c: centre, r } = sk.curves[sk.curve_index("c").unwrap()].kind else { panic!() };
|
||||
assert!((sk.point(centre).unwrap().y.abs() - r).abs() < 1e-7, "{a}, {b}: tangent to x");
|
||||
assert!(pt(&sk, "x").dist(Vec2::new(3.0, 7.0)) < 1e-9, "{a}, {b}: the point called x moved");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_rectangle_centred_on_the_origin_keeps_one_profile() {
|
||||
let mut s = on(json!("XY"));
|
||||
|
||||
@@ -340,6 +340,63 @@ fn tangent_moves_only_the_second_pick() {
|
||||
check_order(line_and_circle(), "sketch.constraint.tangent", "c1", "l1");
|
||||
}
|
||||
|
||||
/// #12092: Tangent with a line's end point and a circle puts that end on the circle and makes the
|
||||
/// line tangent there, in one step. In either pick order only the picked end moves: the line turns
|
||||
/// about its other end and gets longer or shorter, and the circle stays.
|
||||
#[test]
|
||||
fn tangent_at_a_lines_end_puts_the_end_on_the_circle() {
|
||||
// Where the line ends up: its end's distance from the centre, and the line's.
|
||||
let touch = |s: &Session| {
|
||||
let sk = sketch(s);
|
||||
let (a, b) = (pt(s, "l1.start"), pt(s, "l1.end"));
|
||||
let c = sk.point(sk.resolve_point("c1.center").unwrap()).unwrap();
|
||||
let line = (b - a).cross(c - a).abs() / a.dist(b);
|
||||
(b.dist(c), line, sk.radius(sk.curve_index("c1").unwrap()).unwrap())
|
||||
};
|
||||
for (a, b) in [("c1", "l1.end"), ("l1.end", "c1")] {
|
||||
let mut s = line_and_circle();
|
||||
let circle = snap(&s, "c1");
|
||||
let undo = s.undo.len();
|
||||
let r = run(&mut s, "sketch.constraint.tangent", json!({"a": a, "b": b}));
|
||||
assert_eq!(r["constraints"].as_array().map(Vec::len), Some(2), "{a}, {b}: {r}");
|
||||
let (end, line, radius) = touch(&s);
|
||||
assert!((radius - 5.0).abs() < 1e-9, "{a}, {b}: the circle keeps its size: {radius}");
|
||||
assert!((end - 5.0).abs() < 1e-6 && (line - 5.0).abs() < 1e-6, "{a}, {b}: end {end}, line {line} from the centre");
|
||||
assert!(same(&snap(&s, "c1"), &circle), "{a}, {b}: the circle moved");
|
||||
let start = pt(&s, "l1.start");
|
||||
assert!(start.dist(solidcore_record::Vec2::new(5.0, 3.0)) < 1e-9, "{a}, {b}: the line's other end moved to {start:?}");
|
||||
assert_eq!(s.undo.len(), undo + 1, "{a}, {b}: one undo step");
|
||||
}
|
||||
// A point that ends no curve names no curve to be tangent: the refusal says what to pick.
|
||||
let mut s = line_and_circle();
|
||||
let e = s.execute("sketch.constraint.tangent", &json!({"a": "l1", "b": "c1.center"})).unwrap_err().to_string();
|
||||
assert!(e.contains("end"), "{e}");
|
||||
}
|
||||
|
||||
/// A line whose end is already joined to an arc's end (the coincident a click there adds): Tangent
|
||||
/// on that end and the arc, or on the two ends in either order, adds only the tangent, since the
|
||||
/// end already lies on the arc. The joint holds. With the end and the arc the line stays and the
|
||||
/// arc moves; with two ends the first pick stays.
|
||||
#[test]
|
||||
fn tangent_at_a_joint_adds_only_the_tangent() {
|
||||
for (a, b, stays) in [("l1.end", "a1", "l1"), ("a1", "l1.end", "l1"), ("l1.end", "a1.start", "l1"), ("a1.start", "l1.end", "a1")] {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "sketch.line", json!({"points": [[2, 3], [12, 3]]}));
|
||||
run(&mut s, "sketch.arc.center_point", json!({"center": [16, 9], "start": [12, 3], "sweep": 90}));
|
||||
let joints = sketch(&s).constraints.len();
|
||||
let kept = snap(&s, stays);
|
||||
let r = run(&mut s, "sketch.constraint.tangent", json!({"a": a, "b": b}));
|
||||
assert!(same(&snap(&s, stays), &kept), "{a}, {b}: {stays} moved");
|
||||
assert_eq!(r["constraints"].as_array().map(Vec::len), Some(1), "{a}, {b}: {r}");
|
||||
assert_eq!(sketch(&s).constraints.len(), joints + 1, "{a}, {b}: only the tangent is new");
|
||||
let sk = sketch(&s);
|
||||
let arc = sk.curve_index("a1").unwrap();
|
||||
let (p, q, c) = (pt(&s, "l1.start"), pt(&s, "l1.end"), sk.center(arc).unwrap());
|
||||
assert!(q.dist(pt(&s, "a1.start")) < 1e-6, "{a}, {b}: the joint came apart");
|
||||
assert!(((q - p).cross(c - p).abs() / p.dist(q) - sk.radius(arc).unwrap()).abs() < 1e-6, "{a}, {b}: not tangent");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_second_pick_makes_the_first_move() {
|
||||
let mut s = line_and_circle();
|
||||
|
||||
@@ -338,8 +338,8 @@ pub struct Constrained {
|
||||
pub determined_by: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// What `sketch.constraint.horizontal_vertical`, `tangent`, `parallel`, `perpendicular`,
|
||||
/// `collinear`, `concentric` and `midpoint` return.
|
||||
/// What `sketch.constraint.horizontal_vertical`, `parallel`, `perpendicular`, `collinear`,
|
||||
/// `concentric` and `midpoint` return.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct ConstraintResult {
|
||||
#[serde(flatten)]
|
||||
@@ -349,6 +349,35 @@ pub struct ConstraintResult {
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// What `sketch.constraint.tangent` returns.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct TangentResult {
|
||||
#[serde(flatten)]
|
||||
pub made: TangentMade,
|
||||
/// In the warn parameter-check mode: what the call's parameters got wrong.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub param_warnings: Vec<String>,
|
||||
}
|
||||
|
||||
/// What a Tangent call added.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum TangentMade {
|
||||
/// Two curves: one Tangent constraint.
|
||||
Curves(Constrained),
|
||||
/// A curve's end point for `a` or `b` (#12092): the end now lies on the other curve (a
|
||||
/// point-on-curve or, for two end points, a coincident constraint, left out when the end was
|
||||
/// already there) and its curve is tangent there.
|
||||
End {
|
||||
/// The Tangent constraint.
|
||||
constraint: String,
|
||||
/// Every constraint added, the Tangent last.
|
||||
constraints: Vec<String>,
|
||||
/// The sketch's solve: `{solved, dof}`.
|
||||
sketch: Value,
|
||||
},
|
||||
}
|
||||
|
||||
/// What `sketch.constraint.coincident` returns.
|
||||
#[derive(Debug, Serialize, JsonSchema)]
|
||||
pub struct CoincidentResult {
|
||||
@@ -493,10 +522,11 @@ macro_rules! two_operands {
|
||||
}
|
||||
|
||||
two_operands! {
|
||||
/// Make two curves, or a curve and a sketch axis, tangent. Two different curves are needed.
|
||||
/// Make two curves, a curve and a sketch axis, or a curve's end point and another curve,
|
||||
/// tangent. Two different curves are needed.
|
||||
TangentParams,
|
||||
a: "A curve's id, or `x` / `y` for the sketch's own axis (a curve with that id wins; an axis becomes a linked construction line, reported in `created`), or a model edge.",
|
||||
b: "The other curve, as `a`.",
|
||||
a: "A curve's id, or `x` / `y` for the sketch's own axis (a curve with that id wins; an axis becomes a linked construction line, reported in `created`), or a model edge, or the end point of a line or an arc (such as `l1.end`): that end then lies on the other curve, and its curve is tangent there (`constraints` lists both).",
|
||||
b: "The other curve or end point, as `a`.",
|
||||
what: "a curve id, or x / y for a sketch axis"
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use serde_json::{Value, json};
|
||||
use super::{
|
||||
CoincidentParams, CoincidentResult, CollinearParams, ConcentricParams, ConstraintResult, EqualParams, EqualResult, FixParams, FixResult,
|
||||
HorizontalVerticalParams, MidpointParams, Operand, ParallelParams, PerpendicularParams, PointOperand, SketchKey, SymmetryParams, SymmetryResult,
|
||||
TangentParams,
|
||||
TangentParams, TangentResult,
|
||||
};
|
||||
use crate::Session;
|
||||
use crate::schema::tests::{assert_keys, assert_strict_keeps_todays_errors, conforms};
|
||||
@@ -342,9 +342,16 @@ fn replies_match_the_output_schemas_and_real_calls_get_no_warnings() {
|
||||
let r = assert_clean::<CoincidentResult>(&mut s, "sketch.constraint.coincident", json!({"a": "c3", "b": "c4"}));
|
||||
assert!(r["constraints"].as_array().is_some_and(|c| c.len() == 2) && r.get("constraint").is_some(), "{r}");
|
||||
|
||||
// Tangent, parallel, perpendicular, collinear.
|
||||
// Tangent: two curves, and a line's end point and a circle (#12092), which adds two
|
||||
// constraints.
|
||||
let mut s = new();
|
||||
let r = assert_clean::<TangentResult>(&mut s, "sketch.constraint.tangent", json!({"a": "l1", "b": "c1"}));
|
||||
assert!(r.get("constraints").is_none(), "{r}");
|
||||
let r = assert_clean::<TangentResult>(&mut s, "sketch.constraint.tangent", json!({"a": "l3.end", "b": "c2"}));
|
||||
assert!(r["constraints"].as_array().is_some_and(|c| c.len() == 2) && r.get("constraint").is_some(), "{r}");
|
||||
|
||||
// Parallel, perpendicular, collinear.
|
||||
let mut s = new();
|
||||
assert_clean::<ConstraintResult>(&mut s, "sketch.constraint.tangent", json!({"a": "l1", "b": "c1"}));
|
||||
assert_clean::<ConstraintResult>(&mut s, "sketch.constraint.parallel", json!({"a": "l2", "b": "l3"}));
|
||||
assert_clean::<ConstraintResult>(&mut s, "sketch.constraint.parallel", json!({"a": "l4", "b": "x"}));
|
||||
assert_clean::<ConstraintResult>(&mut s, "sketch.constraint.perpendicular", json!({"a": "l5", "b": "l6"}));
|
||||
|
||||
@@ -340,7 +340,7 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2, shift: bool) {
|
||||
}
|
||||
}
|
||||
Kind::Pick(n) => {
|
||||
let hits = pick(app, proj, pos);
|
||||
let hits = curve_operands(tool.cmd, pick(app, proj, pos));
|
||||
// Symmetry's third pick may be an origin axis lying along the sketch's own x or y
|
||||
// (#1075); the command names those `x` / `y`.
|
||||
let axis_ok = tool.cmd == "sketch.constraint.symmetry" && tool.picks.len() == 2;
|
||||
@@ -895,6 +895,20 @@ fn run_pick(app: &mut SolveApp, t: &Tool) {
|
||||
let _ = app.run(t.cmd, params);
|
||||
}
|
||||
|
||||
/// Parallel, Perpendicular, Equal and Collinear constrain two curves, and their commands take no
|
||||
/// point. So a click on a curve's end point picks the curve under it, not the point, and a body's
|
||||
/// edge is taken rather than its vertex (#12092). Tangent also takes a curve's end point: that
|
||||
/// end then lies on the other curve. A body's vertex ends no sketch curve, so Tangent takes the
|
||||
/// edge there too.
|
||||
fn curve_operands(cmd: &str, hits: Vec<Hit>) -> Vec<Hit> {
|
||||
let points = match cmd {
|
||||
"sketch.constraint.collinear" | "sketch.constraint.equal" | "sketch.constraint.parallel" | "sketch.constraint.perpendicular" => false,
|
||||
"sketch.constraint.tangent" => true,
|
||||
_ => return hits,
|
||||
};
|
||||
hits.into_iter().filter(|h| !matches!(h, Hit::Vertex { .. }) && (points || !matches!(h, Hit::SketchPoint { .. }))).collect()
|
||||
}
|
||||
|
||||
/// Take back a pending spline point before undo or Backspace reaches the sketch.
|
||||
pub fn undo_spline_point(app: &mut SolveApp) -> bool {
|
||||
let Some(t) = app.tool.as_mut() else { return false };
|
||||
|
||||
@@ -160,6 +160,48 @@ fn ambiguous_selection_preserves_tool_first_workflow() {
|
||||
}
|
||||
}
|
||||
|
||||
/// #12092: a tool that constrains two curves takes the line when the click lands on its end
|
||||
/// point. It used to pass the point on, and the command refused it ("unknown sketch curve
|
||||
/// `l1.end`"). Tangent keeps the end: the line's end then moves onto the circle, tangent there.
|
||||
#[test]
|
||||
fn a_click_on_a_line_end_picks_the_line_for_two_curve_constraints() {
|
||||
for command in ["parallel", "perpendicular", "equal", "collinear"] {
|
||||
let mut h = lines();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.run(&json!([{"start":format!("sketch.constraint.{command}")}, {"click":{"sketch":[40,10]}}, {"click":{"sketch":[40,35]}}])).unwrap();
|
||||
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{command}: {:?}", h.app.status);
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1, "{command}");
|
||||
assert_line_geometry(&h, command);
|
||||
}
|
||||
let mut h = Harness::new();
|
||||
h.run(&json!([
|
||||
{"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}},
|
||||
{"call":"engine.execute","params":{"command":"sketch.line","params":{"points":[[0,20],[30,20]],"infer":false}}},
|
||||
{"call":"engine.execute","params":{"command":"sketch.circle.center","params":{"center":[50,0],"radius":10}}},
|
||||
{"call":"ui.view","params":{"view":"top"}},
|
||||
{"start":"sketch.constraint.tangent"},
|
||||
{"click":{"sketch":[30,20]}},
|
||||
{"click":{"sketch":[60,0]}}
|
||||
]))
|
||||
.unwrap();
|
||||
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "tangent: {:?}", h.app.status);
|
||||
let sk = &h.app.session.model.state().sketches[0].sketch;
|
||||
assert!(sk.constraints.iter().any(|c| matches!(c.kind, ConstraintKind::Tangent { .. })), "tangent");
|
||||
// The line now touches the circle: its distance from the centre is the radius.
|
||||
let a = sk.point(sk.resolve_point("l1.start").unwrap()).unwrap();
|
||||
let b = sk.point(sk.resolve_point("l1.end").unwrap()).unwrap();
|
||||
let circle = sk.curves.iter().position(|c| matches!(c.kind, solidcraft_engine::sketch::CurveKind::Circle { .. })).unwrap();
|
||||
let centre = sk.point(sk.resolve_point(&format!("{}.center", sk.curves[circle].id)).unwrap()).unwrap();
|
||||
let radius = sk.radius(circle).unwrap();
|
||||
let distance = (b - a).normalized().unwrap().cross(centre - a).abs();
|
||||
assert!((distance - radius).abs() < 1e-6, "line {a:?}..{b:?} is {distance} from the centre, radius {radius}");
|
||||
// The clicked end lies on the circle, where the line touches it.
|
||||
assert!((b.dist(centre) - radius).abs() < 1e-6, "the line's end {b:?} is {} from the centre, radius {radius}", b.dist(centre));
|
||||
// Only that end moved: the line turned about its other end, and the circle stayed.
|
||||
assert!(a.x.abs() < 1e-9 && (a.y - 20.0).abs() < 1e-9, "the line's other end moved to {a:?}");
|
||||
assert!((centre.x - 50.0).abs() < 1e-9 && centre.y.abs() < 1e-9 && (radius - 10.0).abs() < 1e-9, "the circle moved: {centre:?}, radius {radius}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_preselection_preserves_document_and_error() {
|
||||
let mut h = lines();
|
||||
|
||||
Reference in New Issue
Block a user