fix: apply point constraints to sketch preselection
This commit is contained in:
1 parent
9d7be3c292
commit
cc2ee6a0f9
4 files changed
+136
-1
No files matched your search
@@ -545,6 +545,13 @@ impl SolveApp {
|
||||
self.last_command = Some((spec.id.to_string(), spec.label.to_string()));
|
||||
}
|
||||
if let Some(mut t) = tools::Tool::for_command(id) {
|
||||
if let Some(params) = tools::constraint_preselection(id, &self.session.selection) {
|
||||
if self.run(id, params).is_ok() {
|
||||
let _ = self.session.execute("select.clear", &json!({}));
|
||||
self.set_status("Constraint applied", false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Curve-pair constraints can consume an exact preselection. Leave ambiguous
|
||||
// selections to the ordinary pick tool; never silently constrain a subset.
|
||||
if matches!(
|
||||
|
||||
@@ -323,6 +323,41 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Result<Vec<String>, String> {
|
||||
Ok(result["curves"].as_array().into_iter().flatten().filter_map(|c| c.as_str().map(str::to_string)).collect())
|
||||
}
|
||||
|
||||
/// Only consume a complete, unambiguous selection; command validation owns geometry.
|
||||
pub(crate) fn constraint_preselection(command: &str, selection: &[solvecraft_engine::Sel]) -> Option<Value> {
|
||||
use solvecraft_engine::Sel::{SketchCurve, SketchPoint};
|
||||
match (command, selection) {
|
||||
("sketch.constraint.coincident", [a, b]) => {
|
||||
let entity = |s: &solvecraft_engine::Sel| match s {
|
||||
SketchPoint { id } | SketchCurve { id } => Some(id.clone()),
|
||||
_ => None,
|
||||
};
|
||||
let (a, b) = (entity(a)?, entity(b)?);
|
||||
(a != b).then(|| json!({"a":a,"b":b}))
|
||||
}
|
||||
(
|
||||
"sketch.constraint.midpoint",
|
||||
[SketchPoint { id: point }, SketchCurve { id: line }] | [SketchCurve { id: line }, SketchPoint { id: point }],
|
||||
) => Some(json!({"point":point,"line":line})),
|
||||
("sketch.constraint.symmetry", [a, b, c]) => {
|
||||
let mut points = Vec::new();
|
||||
let mut lines = Vec::new();
|
||||
for entity in [a, b, c] {
|
||||
match entity {
|
||||
SketchPoint { id } => points.push(id),
|
||||
SketchCurve { id } => lines.push(id),
|
||||
_ => return None,
|
||||
}
|
||||
}
|
||||
match (points.as_slice(), lines.as_slice()) {
|
||||
([a, b], [line]) if a != b => Some(json!({"a":a,"b":b,"line":line})),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run_pick(app: &mut SolveApp, t: &Tool) {
|
||||
let k = &t.picks;
|
||||
let params = match t.cmd {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
use serde_json::json;
|
||||
use solvecraft_engine::{Sel, sketch::ConstraintKind};
|
||||
use solvecraft_ui_egui::scenario::Harness;
|
||||
|
||||
fn setup() -> Harness {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
h.app.run("sketch.line", json!({"points":[[10,10],[30,10]],"infer":false,"connect":false})).unwrap();
|
||||
h.app.run("sketch.point", json!({"point":[20,20],"id":"a"})).unwrap();
|
||||
h.app.run("sketch.point", json!({"point":[25,30],"id":"b"})).unwrap();
|
||||
h.call("ui.view", json!({"view":"top"}));
|
||||
h
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clicked_point_constraint_preselection_applies_and_undoes_once() {
|
||||
for (command, picks) in [
|
||||
("coincident", vec![[20, 20], [25, 30]]),
|
||||
("coincident", vec![[20, 20], [20, 10]]),
|
||||
("coincident", vec![[20, 10], [20, 20]]),
|
||||
("midpoint", vec![[20, 20], [20, 10]]),
|
||||
("midpoint", vec![[20, 10], [20, 20]]),
|
||||
("symmetry", vec![[20, 10], [20, 20], [25, 30]]),
|
||||
("symmetry", vec![[20, 20], [20, 10], [25, 30]]),
|
||||
] {
|
||||
let mut h = setup();
|
||||
for (index, pick) in picks.iter().enumerate() {
|
||||
h.step(&json!({"click":{"sketch":pick},"shift":index>0})).unwrap();
|
||||
}
|
||||
assert_eq!(h.app.session.selection.len(), picks.len(), "{command}");
|
||||
let before = h.app.session.doc.to_json();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.step(&json!({"start":format!("sketch.constraint.{command}")})).unwrap();
|
||||
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error), "{command}: {:?}", h.app.status);
|
||||
assert!(h.app.tool.is_none());
|
||||
assert!(h.app.session.selection.is_empty());
|
||||
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||
let state = h.app.session.world_state();
|
||||
let sk = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
|
||||
assert!(sk.constraints.iter().any(|c| match command {
|
||||
"coincident" => matches!(c.kind, ConstraintKind::Coincident { .. } | ConstraintKind::PointOnCurve { .. }),
|
||||
"midpoint" => matches!(c.kind, ConstraintKind::Midpoint { .. }),
|
||||
_ => matches!(c.kind, ConstraintKind::Symmetric { .. }),
|
||||
}));
|
||||
let point = |id| sk.point(sk.resolve_point(id).unwrap()).unwrap();
|
||||
let a = point("a");
|
||||
let b = point("b");
|
||||
let start = point("l1.start");
|
||||
let end = point("l1.end");
|
||||
let axis = (end - start).normalized().unwrap();
|
||||
match command {
|
||||
"midpoint" => assert!(a.dist((start + end) * 0.5) < 1e-6),
|
||||
"symmetry" => {
|
||||
assert!(((a + b) * 0.5 - start).cross(axis).abs() < 1e-6);
|
||||
assert!((b - a).dot(axis).abs() < 1e-6);
|
||||
}
|
||||
_ if picks.contains(&[20, 10]) => assert!((a - start).cross(axis).abs() < 1e-6),
|
||||
_ => assert!(a.dist(b) < 1e-6),
|
||||
}
|
||||
let after = h.app.session.doc.to_json();
|
||||
h.app.run("edit.undo", json!({})).unwrap();
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
h.app.run("edit.redo", json!({})).unwrap();
|
||||
assert_eq!(h.app.session.doc.to_json(), after);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejected_and_ambiguous_preselection_preserve_document_and_selection() {
|
||||
let mut h = setup();
|
||||
h.app.run("sketch.constraint.fix", json!({"entities":["a","b"]})).unwrap();
|
||||
h.app.session.selection = vec![Sel::SketchPoint { id: "a".into() }, Sel::SketchPoint { id: "b".into() }];
|
||||
let selection = h.app.session.selection.clone();
|
||||
let before = h.app.session.doc.to_json();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.step(&json!({"start":"sketch.constraint.coincident"})).unwrap();
|
||||
assert!(h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||
assert_eq!(h.app.session.selection, selection);
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
for command in ["coincident", "midpoint", "symmetry"] {
|
||||
h.app.session.selection = vec![Sel::SketchPoint { id: "a".into() }];
|
||||
h.step(&json!({"start":format!("sketch.constraint.{command}")})).unwrap();
|
||||
assert!(h.app.tool.as_ref().unwrap().picks.is_empty());
|
||||
assert_eq!(h.app.session.selection.len(), 1);
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,12 @@ aligned, horizontal, vertical, radius, diameter and angle dimensions. Each is a
|
||||
For Parallel, Perpendicular, Equal, Tangent, Concentric and Collinear, select exactly two
|
||||
sketch curves (Shift-click to add the second) and choose the constraint to apply it immediately.
|
||||
You can also start the constraint first and then pick its curves. An incomplete or mixed
|
||||
selection starts that pick workflow; a rejected constraint keeps the selection for correction.
|
||||
selection leaves the tool ready for ordinary picking.
|
||||
|
||||
Coincident accepts an exact selection of two points or a point and a curve. Midpoint
|
||||
accepts one point and one line, in either selection order. Symmetry accepts two distinct
|
||||
points and one symmetry line, regardless of where the line occurs in the selection.
|
||||
Successful constraints clear the selection; rejected constraints preserve it.
|
||||
|
||||
Under-constrained geometry is blue and fully constrained geometry turns dark, and the status
|
||||
shows the remaining degrees of freedom. A constraint or dimension that would over-constrain the
|
||||
|
||||
Reference in new issue
Block a user