Two-entity constraints move only the second pick (tangent, parallel, perpendicular, equal, concentric, coincident, collinear, symmetric, midpoint): the constraint is applied with the first entity held (solve_holding), the second keeping its size where it can (lines turn and slide, circles translate), then resizing; if the second is fixed, projected or fully constrained the first moves instead, and if neither can the constraint is refused and nothing changes; later solves hold nothing. Context-menu constraints keep the selection order; a curve picked before a point puts the point on the curve
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
3e7826fe56
commit
7f9a8f2afd
5 files changed
+339
-12
No files matched your search
@@ -3,7 +3,7 @@
|
||||
use serde_json::{Value, json};
|
||||
use solvecraft_doc::{FeatureKind, PlaneRef};
|
||||
use solvecraft_geom::{Plane, Vec2, Vec3};
|
||||
use solvecraft_sketch::{ConstraintKind, CurveKind, Sketch, solve};
|
||||
use solvecraft_sketch::{ConstraintKind, CurveKind, Hold, Sketch, SolveReport, solve, solve_holding};
|
||||
|
||||
use super::{CommandSpec, in_sketch};
|
||||
use crate::params::{bad, bool_, expr, num, req_vec2, str_, string_list, vec2, vec3};
|
||||
@@ -833,13 +833,155 @@ fn constrain(s: &mut Session, p: &Value, cmd: &str, f: impl FnOnce(&Sketch) -> R
|
||||
let mut before = None;
|
||||
let (id, info) = edit(s, p, cmd, true, |sk, _| {
|
||||
let k = f(sk)?;
|
||||
before = Some(dof_now(sk));
|
||||
add_c(sk, k)
|
||||
let rep = solve(&mut sk.clone());
|
||||
before = Some(rep.dof);
|
||||
let order = entity_order(sk, p);
|
||||
let id = add_c(sk, k)?;
|
||||
if let Some((first, second)) = order {
|
||||
apply_in_order(sk, &rep, &[first], second, cmd)?;
|
||||
}
|
||||
Ok(id)
|
||||
})?;
|
||||
reject_redundant(before, &info, cmd)?;
|
||||
Ok(json!({"constraint": id, "sketch": info}))
|
||||
}
|
||||
|
||||
/// A sketch entity a constraint is applied to.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub(super) enum Ent {
|
||||
Point(usize),
|
||||
Curve(usize),
|
||||
}
|
||||
|
||||
fn ent(sk: &Sketch, r: &str) -> Option<Ent> {
|
||||
sk.resolve_point(r).map(Ent::Point).or_else(|| sk.curve_index(r).map(Ent::Curve))
|
||||
}
|
||||
|
||||
/// The entities of a two-entity constraint in the order they were picked (`a` then `b`, or
|
||||
/// `point` then `line`).
|
||||
fn entity_order(sk: &Sketch, p: &Value) -> Option<(Ent, Ent)> {
|
||||
let r = |k: &str| p.get(k).and_then(Value::as_str).and_then(|r| ent(sk, r));
|
||||
Some((r("a").or_else(|| r("point"))?, r("b").or_else(|| r("line"))?))
|
||||
}
|
||||
|
||||
/// Held in place: fixed or projected, or fully determined by the constraints already there.
|
||||
fn ent_locked(sk: &Sketch, rep: &SolveReport, e: Ent) -> bool {
|
||||
match e {
|
||||
Ent::Point(i) => sk.point_locked(i) || rep.point_determined.get(i).copied().unwrap_or(false),
|
||||
Ent::Curve(c) => sk.curve_locked(c) || rep.curve_determined.get(c).copied().unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
/// Hold entities: their points, and with `radius` their radii too.
|
||||
fn hold_of(sk: &Sketch, ents: &[Ent], radius: bool) -> Hold {
|
||||
let mut h = Hold::default();
|
||||
for e in ents {
|
||||
match *e {
|
||||
Ent::Point(i) => h.points.push(i),
|
||||
Ent::Curve(c) => {
|
||||
h.points.extend(sk.curves.get(c).map(|c| c.kind.point_ids()).unwrap_or_default());
|
||||
h.radii.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
if !radius {
|
||||
h.radii.clear();
|
||||
}
|
||||
h
|
||||
}
|
||||
|
||||
/// Did a solve shrink a line or an arc to nothing (a degenerate way to satisfy tangency)?
|
||||
fn collapsed(before: &Sketch, after: &Sketch) -> bool {
|
||||
let size = |sk: &Sketch, i: usize| match sk.curves.get(i).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { .. }) => line_pts(sk, i).map(|(a, b)| a.dist(b)),
|
||||
Some(CurveKind::Arc { .. } | CurveKind::Circle { .. }) => sk.radius(i),
|
||||
_ => None,
|
||||
};
|
||||
(0..before.curves.len()).any(|i| match (size(before, i), size(after, i)) {
|
||||
(Some(b), Some(a)) => b > 1e-6 && a < b * 1e-3,
|
||||
_ => false,
|
||||
})
|
||||
}
|
||||
|
||||
/// Keep an entity's size while it moves: a line's length (circles keep their radius by holding
|
||||
/// it).
|
||||
fn keep_size(sk: &Sketch, e: Ent) -> Vec<ConstraintKind> {
|
||||
match (
|
||||
e,
|
||||
line_pts(
|
||||
sk,
|
||||
match e {
|
||||
Ent::Curve(c) => c,
|
||||
Ent::Point(_) => usize::MAX,
|
||||
},
|
||||
),
|
||||
) {
|
||||
(Ent::Curve(l), Some((a, b))) => vec![ConstraintKind::Length { l, value: a.dist(b) }],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Satisfy a just-added constraint the way Fusion applies it: the first-picked entities stay
|
||||
/// put and only the second moves (first keeping its size, so it turns and slides, then also
|
||||
/// resizing).
|
||||
/// If the second can't move (fixed, projected or fully determined), the first moves instead;
|
||||
/// if neither can, the constraint conflicts and nothing changes. Everything else moves as
|
||||
/// little as it can. Later solves are ordinary ones (nothing is held).
|
||||
pub(super) fn apply_in_order(sk: &mut Sketch, rep: &SolveReport, first: &[Ent], second: Ent, cmd: &str) -> Result<()> {
|
||||
let first_locked = first.iter().any(|e| ent_locked(sk, rep, *e));
|
||||
let second_locked = ent_locked(sk, rep, second);
|
||||
// (held geometry, temporary size constraints) to try in turn.
|
||||
let mut tries: Vec<(Hold, Vec<ConstraintKind>)> = Vec::new();
|
||||
let mut ordered = |stay: &[Ent], mover: Ent| {
|
||||
let mut h = hold_of(sk, stay, true);
|
||||
h.radii.extend(hold_of(sk, &[mover], true).radii);
|
||||
tries.push((h.clone(), keep_size(sk, mover)));
|
||||
tries.push((hold_of(sk, stay, true), Vec::new()));
|
||||
// An end point the two share may slide (a line tangent to an arc it ends on, whose
|
||||
// centre is pinned, has to turn about its other end).
|
||||
let shared = hold_of(sk, &[mover], false).points;
|
||||
if h.points.iter().any(|p| shared.contains(p)) {
|
||||
h.points.retain(|p| !shared.contains(p));
|
||||
tries.push((h.clone(), keep_size(sk, mover)));
|
||||
tries.push((h, Vec::new()));
|
||||
}
|
||||
};
|
||||
if !second_locked {
|
||||
ordered(first, second);
|
||||
}
|
||||
if !first_locked {
|
||||
for f in first {
|
||||
ordered(&[second], *f);
|
||||
}
|
||||
}
|
||||
const TEMP: &str = "__keep_size";
|
||||
for (h, extra) in tries {
|
||||
let mut trial = sk.clone();
|
||||
trial.constraints.extend(extra.into_iter().map(|kind| solvecraft_sketch::Constraint {
|
||||
id: TEMP.into(),
|
||||
kind,
|
||||
param: None,
|
||||
driven: false,
|
||||
text: None,
|
||||
}));
|
||||
let ok = solve_holding(&mut trial, &h).ok();
|
||||
trial.constraints.retain(|c| c.id != TEMP);
|
||||
if ok && !collapsed(sk, &trial) {
|
||||
*sk = trial;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
// Both could move but holding either one leads nowhere: solve as usual.
|
||||
if !first_locked && !second_locked {
|
||||
let mut trial = sk.clone();
|
||||
if solve(&mut trial).ok() {
|
||||
*sk = trial;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(bad(cmd, "that conflicts with the sketch: neither entity can move to satisfy it (they are fixed or fully constrained)"))
|
||||
}
|
||||
|
||||
/// Degrees of freedom of a sketch as it is.
|
||||
pub(super) fn dof_now(sk: &Sketch) -> usize {
|
||||
solve(&mut sk.clone()).dof
|
||||
@@ -890,8 +1032,13 @@ fn c_horizontal_vertical(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
fn c_coincident(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let cmd = "ConstraintCoincident";
|
||||
constrain(s, p, cmd, |sk| {
|
||||
let a = point_ref(sk, p.get("a"), cmd, "a")?;
|
||||
let aref = p.get("a").and_then(Value::as_str).unwrap_or_default();
|
||||
let bref = p.get("b").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`b` must be a point or curve"))?;
|
||||
// A curve picked first and a point second: the point goes onto the curve.
|
||||
if let (None, Some(c), Some(q)) = (sk.resolve_point(aref), sk.curve_index(aref), sk.resolve_point(bref)) {
|
||||
return Ok(ConstraintKind::PointOnCurve { p: q, c });
|
||||
}
|
||||
let a = point_ref(sk, p.get("a"), cmd, "a")?;
|
||||
if let Some(b) = sk.resolve_point(bref) {
|
||||
return Ok(ConstraintKind::Coincident { p: a, q: b });
|
||||
}
|
||||
@@ -934,6 +1081,8 @@ fn c_symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let l = curve_ref(sk, p.get("line"), cmd, "line")?;
|
||||
let ra = p.get("a").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`a` must be a point or curve"))?;
|
||||
let rb = p.get("b").and_then(Value::as_str).ok_or_else(|| bad(cmd, "`b` must be a point or curve"))?;
|
||||
let rep = solve(&mut sk.clone());
|
||||
let order = ent(sk, ra).zip(ent(sk, rb));
|
||||
let mut out = Vec::new();
|
||||
match (sk.resolve_point(ra), sk.resolve_point(rb)) {
|
||||
(Some(a), Some(b)) => out.push(add_c(sk, ConstraintKind::Symmetric { p: a, q: b, l })?),
|
||||
@@ -957,6 +1106,10 @@ fn c_symmetry(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The first entity and the symmetry line stay; the second mirrors onto it.
|
||||
if let Some((a, b)) = order {
|
||||
apply_in_order(sk, &rep, &[a, Ent::Curve(l)], b, cmd)?;
|
||||
}
|
||||
Ok(out)
|
||||
})?;
|
||||
Ok(json!({"constraints": ids, "sketch": info}))
|
||||
|
||||
@@ -252,3 +252,151 @@ fn projected_geometry_can_become_construction() {
|
||||
let sk = sketch(&s);
|
||||
assert!(sk.curves.iter().any(|c| c.id == id && c.construction && c.link.is_some()));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Two-entity constraints: the first pick stays, the second moves
|
||||
|
||||
/// Positions of an entity's points (and its radius).
|
||||
fn snap(s: &Session, id: &str) -> (Vec<solvecraft_geom::Vec2>, Option<f64>) {
|
||||
let sk = sketch(s);
|
||||
if let Some(i) = sk.curve_index(id) {
|
||||
let pts = sk.curves[i].kind.point_ids().into_iter().filter_map(|p| sk.point(p)).collect();
|
||||
return (pts, sk.radius(i));
|
||||
}
|
||||
(vec![pt(s, id)], None)
|
||||
}
|
||||
|
||||
fn same(a: &(Vec<solvecraft_geom::Vec2>, Option<f64>), b: &(Vec<solvecraft_geom::Vec2>, Option<f64>)) -> bool {
|
||||
a.0.len() == b.0.len() && a.0.iter().zip(&b.0).all(|(p, q)| p.dist(*q) < 1e-9) && a.1.zip(b.1).is_none_or(|(x, y)| (x - y).abs() < 1e-9)
|
||||
}
|
||||
|
||||
/// A free line l1 (5,3)–(45,3) and a circle c1 at (20,18) with radius 5.
|
||||
fn line_and_circle() -> Session {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[5, 3], [45, 3]]}));
|
||||
run(&mut s, "CircleCenterRadius", json!({"center": [20, 18], "radius": 5}));
|
||||
s
|
||||
}
|
||||
|
||||
/// Apply `cmd` with `a` then `b`; the first must not move, the second must.
|
||||
fn check_order(mut s: Session, cmd: &str, a: &str, b: &str) {
|
||||
let (a0, b0) = (snap(&s, a), snap(&s, b));
|
||||
run(&mut s, cmd, json!({"a": a, "b": b}));
|
||||
assert!(same(&snap(&s, a), &a0), "{cmd}: first `{a}` moved: {a0:?} -> {:?}", snap(&s, a));
|
||||
assert!(!same(&snap(&s, b), &b0), "{cmd}: second `{b}` did not move");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_moves_only_the_second_pick() {
|
||||
// Line then circle: only the circle moves (and keeps its size).
|
||||
let mut s = line_and_circle();
|
||||
let l0 = snap(&s, "l1");
|
||||
run(&mut s, "ConstraintTangent", json!({"a": "l1", "b": "c1"}));
|
||||
assert!(same(&snap(&s, "l1"), &l0), "line moved: {:?}", snap(&s, "l1"));
|
||||
let c = snap(&s, "c1");
|
||||
assert!((c.1.unwrap_or(0.0) - 5.0).abs() < 1e-9 && (c.0[0].y - 8.0).abs() < 1e-6, "{c:?}");
|
||||
// Circle then line: only the line moves.
|
||||
check_order(line_and_circle(), "ConstraintTangent", "c1", "l1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_fixed_second_pick_makes_the_first_move() {
|
||||
let mut s = line_and_circle();
|
||||
run(&mut s, "ConstraintFix", json!({"entity": "l1"}));
|
||||
let l0 = snap(&s, "l1");
|
||||
let c0 = snap(&s, "c1");
|
||||
run(&mut s, "ConstraintTangent", json!({"a": "c1", "b": "l1"}));
|
||||
assert!(same(&snap(&s, "l1"), &l0));
|
||||
assert!(!same(&snap(&s, "c1"), &c0));
|
||||
// Fixed first: the second moves as usual.
|
||||
let mut s = line_and_circle();
|
||||
run(&mut s, "ConstraintFix", json!({"entity": "c1"}));
|
||||
let c0 = snap(&s, "c1");
|
||||
run(&mut s, "ConstraintTangent", json!({"a": "c1", "b": "l1"}));
|
||||
assert!(same(&snap(&s, "c1"), &c0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_fully_constrained_entities_conflict_and_nothing_moves() {
|
||||
let mut s = line_and_circle();
|
||||
run(&mut s, "ConstraintFix", json!({"entities": ["l1.start", "l1.end", "c1.center"]}));
|
||||
run(&mut s, "SketchDimension", json!({"entities": ["c1"], "type": "radius", "value": 5}));
|
||||
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||
assert!(si["curves"].as_array().is_some_and(|c| c.iter().all(|c| c["fully_constrained"] == true)), "{si}");
|
||||
let before = sketch(&s);
|
||||
let e = s.execute("ConstraintTangent", &json!({"a": "l1", "b": "c1"}));
|
||||
assert!(e.is_err(), "{e:?}");
|
||||
assert_eq!(sketch(&s), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_pairs_keep_the_first_line() {
|
||||
let lines = || {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 2]]}));
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[5, 20], [25, 35]]}));
|
||||
s
|
||||
};
|
||||
let len = |s: &Session, l: &str| {
|
||||
let p = snap(s, l).0;
|
||||
p[0].dist(p[1])
|
||||
};
|
||||
for cmd in ["ConstraintParallel", "ConstraintPerpendicular", "ConstraintEqual", "ConstraintCollinear"] {
|
||||
check_order(lines(), cmd, "l1", "l2");
|
||||
check_order(lines(), cmd, "l2", "l1");
|
||||
// The second line turns and slides; only Equal changes its length.
|
||||
let mut s = lines();
|
||||
let l0 = len(&s, "l2");
|
||||
run(&mut s, cmd, json!({"a": "l1", "b": "l2"}));
|
||||
if cmd != "ConstraintEqual" {
|
||||
assert!((len(&s, "l2") - l0).abs() < 1e-6, "{cmd}: {l0} -> {}", len(&s, "l2"));
|
||||
} else {
|
||||
assert!((len(&s, "l2") - len(&s, "l1")).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn circle_pairs_keep_the_first_circle() {
|
||||
let circles = || {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "CircleCenterRadius", json!({"center": [0, 0], "radius": 5}));
|
||||
run(&mut s, "CircleCenterRadius", json!({"center": [30, 10], "radius": 8}));
|
||||
s
|
||||
};
|
||||
for cmd in ["ConstraintConcentric", "ConstraintEqual", "ConstraintTangent"] {
|
||||
check_order(circles(), cmd, "c1", "c2");
|
||||
check_order(circles(), cmd, "c2", "c1");
|
||||
}
|
||||
// Equal resizes the second to the first.
|
||||
let mut s = circles();
|
||||
run(&mut s, "ConstraintEqual", json!({"a": "c2", "b": "c1"}));
|
||||
assert!((snap(&s, "c1").1.unwrap_or(0.0) - 8.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_constraints_keep_the_first_pick() {
|
||||
let pts = || {
|
||||
let mut s = new_sketch();
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [30, 0]]}));
|
||||
run(&mut s, "DrawPolyline", json!({"points": [[10, 10], [20, 25]]}));
|
||||
s
|
||||
};
|
||||
check_order(pts(), "ConstraintCoincident", "l2.start", "l1.end");
|
||||
check_order(pts(), "ConstraintCoincident", "l1.end", "l2.start");
|
||||
// Curve picked first, point second: the point goes onto the line.
|
||||
check_order(pts(), "ConstraintCoincident", "l1", "l2.start");
|
||||
// Midpoint: the point stays, the line moves.
|
||||
let mut s = pts();
|
||||
let p0 = snap(&s, "l2.end");
|
||||
run(&mut s, "ConstraintMidPoint", json!({"point": "l2.end", "line": "l1"}));
|
||||
assert!(same(&snap(&s, "l2.end"), &p0));
|
||||
// Symmetry: the first point and the line stay, the second mirrors.
|
||||
let mut s = pts();
|
||||
run(&mut s, "DrawPoint", json!({"point": [3, 7]}));
|
||||
run(&mut s, "DrawPoint", json!({"point": [4, -9]}));
|
||||
let (a0, l0) = (snap(&s, "p1"), snap(&s, "l1"));
|
||||
run(&mut s, "ConstraintSymmetry", json!({"a": "p1", "b": "p2", "line": "l1"}));
|
||||
assert!(same(&snap(&s, "p1"), &a0) && same(&snap(&s, "l1"), &l0));
|
||||
assert!(pt(&s, "p2").dist(solvecraft_geom::Vec2::new(3.0, -7.0)) < 1e-6, "{:?}", pt(&s, "p2"));
|
||||
}
|
||||
@@ -27,7 +27,7 @@ pub use link::{Link, LinkGeom, LinkKind, LinkSource, MAX_LINK_ENTITIES};
|
||||
pub use model::{Constraint, ConstraintKind, Curve, CurveKind, SPoint, Sketch, SketchError};
|
||||
pub use profiles::{Profile, find_drawn_profiles, find_profiles, merge_regions};
|
||||
pub use shape::{Shape, intersections};
|
||||
pub use solver::{SolveReport, SolveStatus, solve};
|
||||
pub use solver::{Hold, SolveReport, SolveStatus, solve, solve_holding};
|
||||
pub use text::{MAX_TEXT_CHARS, text_geometry};
|
||||
pub use wire::{MAX_WIRE_POINTS, Wire};
|
||||
|
||||
|
||||
@@ -61,8 +61,20 @@ struct Layout {
|
||||
n: usize,
|
||||
}
|
||||
|
||||
fn layout(sk: &Sketch) -> Layout {
|
||||
/// Geometry held in place for one solve: point indices and curves whose radius is held.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct Hold {
|
||||
pub points: Vec<usize>,
|
||||
pub radii: Vec<usize>,
|
||||
}
|
||||
|
||||
fn layout(sk: &Sketch, hold: &Hold) -> Layout {
|
||||
let mut fixed: Vec<bool> = sk.points.iter().map(|p| p.fixed).collect();
|
||||
for p in &hold.points {
|
||||
if let Some(f) = fixed.get_mut(*p) {
|
||||
*f = true;
|
||||
}
|
||||
}
|
||||
for c in &sk.constraints {
|
||||
if let ConstraintKind::Fix { p } = c.kind
|
||||
&& let Some(f) = fixed.get_mut(p)
|
||||
@@ -92,8 +104,9 @@ fn layout(sk: &Sketch) -> Layout {
|
||||
let rvar = sk
|
||||
.curves
|
||||
.iter()
|
||||
.map(|c| match c.kind {
|
||||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } if c.link.is_none() && !c.fixed => {
|
||||
.enumerate()
|
||||
.map(|(i, c)| match c.kind {
|
||||
CurveKind::Circle { .. } | CurveKind::Ellipse { .. } if c.link.is_none() && !c.fixed && !hold.radii.contains(&i) => {
|
||||
n += 1;
|
||||
Some(n - 1)
|
||||
}
|
||||
@@ -774,7 +787,14 @@ fn lm(sk: &Sketch, lay: &Layout, blocks: &[(Block, Vec<usize>)], vars: &[usize],
|
||||
/// Solve the sketch in place. On failure the geometry is restored and the report lists the
|
||||
/// violated constraints.
|
||||
pub fn solve(sk: &mut Sketch) -> SolveReport {
|
||||
let lay = layout(sk);
|
||||
solve_holding(sk, &Hold::default())
|
||||
}
|
||||
|
||||
/// Solve with some geometry held as if it were fixed (for this solve only): applying a
|
||||
/// constraint between two entities holds the first so only the second moves. Everything else
|
||||
/// moves as little as it can. On failure the geometry is restored.
|
||||
pub fn solve_holding(sk: &mut Sketch, hold: &Hold) -> SolveReport {
|
||||
let lay = layout(sk, hold);
|
||||
let mut x = vec![0.0; lay.n];
|
||||
for (i, p) in sk.points.iter().enumerate() {
|
||||
if let Some(v) = lay.pvar.get(i).copied().flatten() {
|
||||
|
||||
@@ -421,9 +421,12 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
|
||||
let mut rounds = Vec::new();
|
||||
let mut curves = Vec::new();
|
||||
let mut points = Vec::new();
|
||||
// Selection order: a two-entity constraint keeps the first and moves the second.
|
||||
let mut picked: Vec<String> = Vec::new();
|
||||
for s in &app.session.selection {
|
||||
match s {
|
||||
Sel::SketchCurve { id } => {
|
||||
picked.push(id.clone());
|
||||
curves.push(id.clone());
|
||||
match ss.sketch.curve_index(id).and_then(|i| ss.sketch.curves.get(i)).map(|c| &c.kind) {
|
||||
Some(CurveKind::Line { .. }) => lines.push(id.clone()),
|
||||
@@ -431,7 +434,10 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Sel::SketchPoint { id } => points.push(id.clone()),
|
||||
Sel::SketchPoint { id } => {
|
||||
picked.push(id.clone());
|
||||
points.push(id.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -461,7 +467,7 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
|
||||
c.push(cmd(app, "ConstraintHorizontalVertical", "Horizontal/Vertical").with(json!({ "points": points })));
|
||||
}
|
||||
if p == 1 && curves.len() == 1 && n == 2 {
|
||||
c.push(cmd(app, "ConstraintCoincident", "Coincident").with(json!({ "a": points[0], "b": curves[0] })));
|
||||
c.push(cmd(app, "ConstraintCoincident", "Coincident").with(json!({ "a": picked[0], "b": picked[1] })));
|
||||
if l == 1 {
|
||||
c.push(cmd(app, "ConstraintMidPoint", "MidPoint").with(json!({ "point": points[0], "line": lines[0] })));
|
||||
}
|
||||
@@ -480,7 +486,7 @@ fn sketch_entity_items(app: &SolveApp) -> Vec<Item> {
|
||||
c.push(cmd(app, "ConstraintTangent", "Tangent").with(ab));
|
||||
}
|
||||
if l == 1 && r == 1 && n == 2 {
|
||||
c.push(cmd(app, "ConstraintTangent", "Tangent").with(json!({ "a": lines[0], "b": rounds[0] })));
|
||||
c.push(cmd(app, "ConstraintTangent", "Tangent").with(json!({ "a": picked[0], "b": picked[1] })));
|
||||
}
|
||||
if p == 2 && l == 1 && n == 3 {
|
||||
c.push(cmd(app, "ConstraintSymmetry", "Symmetry").with(json!({ "a": points[0], "b": points[1], "line": lines[0] })));
|
||||
|
||||
Reference in new issue
Block a user