Apply remaining sketch constraints to complete preselection
This commit is contained in:
1 parent
ed102b5393
commit
db0a2a007a
4 files changed
+124
-16
No files matched your search
@@ -67,6 +67,9 @@ headline counts in-scope tabs only and lists the deferred ones separately.
|
||||
Type a value and press Enter to commit one undo step; rejected values remain editable and
|
||||
cancellation leaves the sketch unchanged. Fit keeps geometry clear of the Sketch Palette
|
||||
and navigation controls, including enlarged displays and nested pending placements.
|
||||
- Curvature and Polygon constraints accept complete preselected operands. Fix/Unfix
|
||||
consumes complete point/curve selections consistently; mixed or ambiguous selections
|
||||
retain the picking workflow. Rejected constraints preserve selection and undo history.
|
||||
- Contextual error reporting: a **Create Issue** action beside feature errors opens a
|
||||
reviewable GitHub issue draft with the error, operation settings, build diagnostics and
|
||||
useful reproduction context. The draft is editable, with optional capture of the actual
|
||||
|
||||
@@ -519,22 +519,6 @@ impl SolveApp {
|
||||
let edges = self.session.selection.iter().any(|s| matches!(s, solvecraft_engine::Sel::Edge { .. }));
|
||||
return self.start(if edges { "solid.fillet" } else { "solid.extrude" });
|
||||
}
|
||||
// Fix/Unfix: the selected sketch points and curves change at once, otherwise pick them.
|
||||
if id == "sketch.constraint.fix" {
|
||||
let ents: Vec<String> = self
|
||||
.session
|
||||
.selection
|
||||
.iter()
|
||||
.filter_map(|s| match s {
|
||||
solvecraft_engine::Sel::SketchCurve { id } | solvecraft_engine::Sel::SketchPoint { id } => Some(id.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if !ents.is_empty() {
|
||||
let _ = self.run(id, json!({ "entities": ents }));
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Construction toggle: selected sketch curves switch at once, otherwise pick them.
|
||||
// Construction (X): convert the selection, or switch construction mode; no pick tool.
|
||||
if id == "sketch.construction" {
|
||||
|
||||
@@ -326,7 +326,25 @@ fn run_shape(app: &mut SolveApp, t: &Tool) -> Result<Vec<String>, String> {
|
||||
/// 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};
|
||||
let selected_ids = |curves_only: bool, minimum: usize| -> Option<Vec<&str>> {
|
||||
if !(minimum..=200).contains(&selection.len()) {
|
||||
return None;
|
||||
}
|
||||
let ids: Vec<&str> = selection
|
||||
.iter()
|
||||
.map(|s| match s {
|
||||
SketchCurve { id } => Some(id.as_str()),
|
||||
SketchPoint { id } if !curves_only => Some(id.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Option<_>>()?;
|
||||
let unique: std::collections::BTreeSet<_> = ids.iter().copied().collect();
|
||||
(unique.len() == ids.len()).then_some(ids)
|
||||
};
|
||||
match (command, selection) {
|
||||
("sketch.constraint.fix", _) => selected_ids(false, 1).map(|ids| json!({"entities":ids})),
|
||||
("sketch.constraint.polygon", _) => selected_ids(true, 3).map(|ids| json!({"lines":ids})),
|
||||
("sketch.constraint.curvature", [SketchCurve { id: a }, SketchCurve { id: b }]) if a != b => Some(json!({"a":a,"b":b})),
|
||||
("sketch.constraint.horizontal_vertical", [SketchCurve { id }]) => Some(json!({"line":id})),
|
||||
("sketch.constraint.horizontal_vertical", [SketchPoint { id: a }, SketchPoint { id: b }]) if a != b => Some(json!({"points":[a,b]})),
|
||||
("sketch.constraint.coincident", [a, b]) => {
|
||||
|
||||
@@ -146,3 +146,106 @@ fn rejected_preselection_preserves_document_and_error() {
|
||||
assert!(!h.app.status.as_ref().is_some_and(|(_, _, error)| *error));
|
||||
assert!(h.app.session.selection.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn curvature_polygon_and_fix_consume_clicked_operands_atomically() {
|
||||
for command in ["fix", "curvature", "polygon"] {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||
let picks = match command {
|
||||
"curvature" => {
|
||||
h.app.run("sketch.line", json!({"points":[[10,10],[30,10]],"infer":false})).unwrap();
|
||||
h.app.run("sketch.spline.control_point", json!({"points":["l1.end",[40,12],[50,16],[60,25]]})).unwrap();
|
||||
vec![[20., 10.], [45., 14.875]]
|
||||
}
|
||||
"polygon" => {
|
||||
h.app.run("sketch.line", json!({"points":[[10,10],[40,10],[40,30],[10,30]],"closed":true,"infer":false})).unwrap();
|
||||
vec![[25., 10.], [40., 20.], [25., 30.], [10., 20.]]
|
||||
}
|
||||
_ => {
|
||||
h.app.run("sketch.line", json!({"points":[[10,10],[40,10]],"infer":false})).unwrap();
|
||||
h.app.run("sketch.point", json!({"point":[20,25],"id":"p"})).unwrap();
|
||||
vec![[25., 10.], [20., 25.]]
|
||||
}
|
||||
};
|
||||
h.call("ui.view", json!({"view":"top"}));
|
||||
h.app.cam.distance = 80.0;
|
||||
h.app.cam.target = solvecraft_engine::geom::Vec3::new(30.0, 15.0, 0.0);
|
||||
h.settle();
|
||||
for (i, p) in picks.iter().enumerate() {
|
||||
h.step(&json!({"click":{"sketch":p},"shift":i>0})).unwrap();
|
||||
}
|
||||
assert_eq!(h.app.session.selection.len(), picks.len(), "{command}: {:?}", h.app.session.selection);
|
||||
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 st = h.app.session.world_state();
|
||||
let sk = &st.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
|
||||
assert!(sk.constraints.iter().any(|c| match command {
|
||||
"fix" => matches!(c.kind, ConstraintKind::Fix { .. }),
|
||||
"curvature" => matches!(c.kind, ConstraintKind::Smooth { .. }),
|
||||
_ => matches!(c.kind, ConstraintKind::Equal { .. }),
|
||||
}));
|
||||
match command {
|
||||
"fix" => {
|
||||
assert!(sk.curve_locked(sk.curve_index("l1").unwrap()));
|
||||
assert!(sk.point_locked(sk.resolve_point("p").unwrap()));
|
||||
}
|
||||
"curvature" => {
|
||||
let spline = sk.curves.iter().find(|c| c.id != "l1").unwrap();
|
||||
let pts: Vec<_> = spline.kind.point_ids().iter().map(|i| sk.point(*i).unwrap()).collect();
|
||||
let line = sk.polyline(sk.curve_index("l1").unwrap());
|
||||
let direction = (line[1] - line[0]).normalized().unwrap();
|
||||
assert!((pts[1] - pts[0]).cross(direction).abs() < 1e-5);
|
||||
assert!((pts[2] - pts[1]).cross(pts[1] - pts[0]).abs() < 1e-3);
|
||||
}
|
||||
_ => {
|
||||
let lengths: Vec<_> = ["l1", "l2", "l3", "l4"]
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let pts = sk.polyline(sk.curve_index(id).unwrap());
|
||||
pts[0].dist(pts[1])
|
||||
})
|
||||
.collect();
|
||||
assert!(lengths[0] > 1.0);
|
||||
assert!(lengths.iter().all(|l| (l - lengths[0]).abs() < 1e-6));
|
||||
}
|
||||
}
|
||||
assert!(st.sketch(h.app.session.active_sketch.unwrap()).unwrap().report.ok());
|
||||
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 remaining_constraints_preserve_mixed_selections_and_rejected_geometry() {
|
||||
for command in ["fix", "curvature", "polygon"] {
|
||||
let mut h = lines();
|
||||
let before = h.app.session.doc.to_json();
|
||||
let undo = h.app.session.undo.len();
|
||||
let mixed = vec![Sel::SketchCurve { id: "l1".into() }, Sel::Body { name: "missing".into() }];
|
||||
h.app.session.selection = mixed.clone();
|
||||
h.step(&json!({"start":format!("sketch.constraint.{command}")})).unwrap();
|
||||
assert!(h.app.tool.is_some());
|
||||
assert_eq!(h.app.session.selection, mixed);
|
||||
assert_eq!(h.app.session.doc.to_json(), before);
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
}
|
||||
let mut h = lines();
|
||||
let selection = vec![Sel::SketchCurve { id: "l1".into() }, Sel::SketchCurve { id: "missing".into() }];
|
||||
h.app.session.selection = selection.clone();
|
||||
let before = h.app.session.doc.to_json();
|
||||
let undo = h.app.session.undo.len();
|
||||
h.step(&json!({"start":"sketch.constraint.curvature"})).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);
|
||||
}
|
||||
Reference in new issue
Block a user