Place a dimension that would over-constrain the sketch as a driven one (#366)
Sketch Dimension refused a dimension the sketch couldn't take, with "that dimension would over-constrain the sketch; add it as a driven (reference) dimension with driven: true", which the tool gave no way to do. It now places it as a driven dimension instead: while placing, the preview shows it in parentheses in the driven colour, and the click adds it at once (it has no parameter and no value to edit), with a status line saying why. Other refusals, such as an unsupported pick, are reported as before, since the driven dimension is refused too. Refs #320 Co-authored-by: Vitali M. <6042958+vitalibr@users.noreply.github.com>
This commit is contained in:
1 parent
c51735c885
commit
6efbd6ff14
3 files changed
+100
-6
No files matched your search
@@ -63,6 +63,9 @@ struct DimensionDraft {
|
|||||||
preview_doc: Arc<solvecraft_engine::doc::Document>,
|
preview_doc: Arc<solvecraft_engine::doc::Document>,
|
||||||
preview: Arc<solvecraft_engine::doc::ModelState>,
|
preview: Arc<solvecraft_engine::doc::ModelState>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
|
/// A dimension that would over-constrain the sketch, placed as a driven (reference) one:
|
||||||
|
/// it has no parameter and no value to edit (#320).
|
||||||
|
driven: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DimensionDraft {
|
impl DimensionDraft {
|
||||||
@@ -102,8 +105,15 @@ pub fn cancel() {
|
|||||||
|
|
||||||
/// Infer the measured value through the same command as scripts, without creating a real
|
/// Infer the measured value through the same command as scripts, without creating a real
|
||||||
/// parameter or an undo entry. Only accepting the editor executes the command on the design.
|
/// parameter or an undo entry. Only accepting the editor executes the command on the design.
|
||||||
pub fn begin_dimension(app: &SolveApp, entities: Vec<String>, at: Vec2) -> Result<(), String> {
|
/// A driven dimension has no value to accept, so it is added at once.
|
||||||
|
pub fn begin_dimension(app: &mut SolveApp, entities: Vec<String>, at: Vec2) -> Result<(), String> {
|
||||||
let (param, expr, draft) = dimension_draft(app, entities, at)?;
|
let (param, expr, draft) = dimension_draft(app, entities, at)?;
|
||||||
|
if draft.driven {
|
||||||
|
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
||||||
|
app.run("sketch.dimension", json!({"entities": draft.entities, "text_at": [at.x, at.y], "driven": true}))?;
|
||||||
|
app.set_status("That dimension would over-constrain the sketch, so it was added as a driven (reference) dimension", false);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft) }));
|
EDIT.with(|edit| *edit.borrow_mut() = Some(Edit { param, expr, at: None, fresh: FRESH, wait: 10, draft: Some(draft) }));
|
||||||
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
PLACEMENT.with(|preview| *preview.borrow_mut() = None);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -114,22 +124,41 @@ fn dimension_draft(app: &SolveApp, entities: Vec<String>, at: Vec2) -> Result<(S
|
|||||||
if entities.is_empty() || entities.len() > 2 || !at.x.is_finite() || !at.y.is_finite() {
|
if entities.is_empty() || entities.len() > 2 || !at.x.is_finite() || !at.y.is_finite() {
|
||||||
return Err("Pick one edge or two endpoints, then place the dimension".into());
|
return Err("Pick one edge or two endpoints, then place the dimension".into());
|
||||||
}
|
}
|
||||||
|
// A dimension the sketch can't take (it would over-constrain it) becomes a driven one,
|
||||||
|
// which only measures; when that fails too, the first refusal is the one to report.
|
||||||
|
draft_of(app, sketch, &entities, at, false).or_else(|error| draft_of(app, sketch, &entities, at, true).map_err(|_| error))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The draft of one dimension: its parameter (for a driven one, its constraint id, since it
|
||||||
|
/// has no parameter), the parameter's expression, and the scratch session it was added in.
|
||||||
|
fn draft_of(app: &SolveApp, sketch: u64, entities: &[String], at: Vec2, driven: bool) -> Result<(String, String, DimensionDraft), String> {
|
||||||
let mut scratch = app.session.scratch();
|
let mut scratch = app.session.scratch();
|
||||||
let result = scratch.execute("sketch.dimension", &json!({"entities": entities, "text_at": [at.x, at.y]})).map_err(|e| e.to_string())?;
|
let result =
|
||||||
let param = result.get("param").and_then(serde_json::Value::as_str).ok_or("This dimension has no editable value")?.to_string();
|
scratch.execute("sketch.dimension", &json!({"entities": entities, "text_at": [at.x, at.y], "driven": driven})).map_err(|e| e.to_string())?;
|
||||||
let expr = scratch.doc.param(¶m).ok_or("The dimension value is unavailable")?.expr.clone();
|
let (param, expr) = if driven {
|
||||||
|
let before =
|
||||||
|
app.session.doc.sketch(sketch).ok().map(|sk| sk.constraints.iter().map(|c| c.id.clone()).collect::<Vec<_>>()).unwrap_or_default();
|
||||||
|
let added =
|
||||||
|
scratch.doc.sketch(sketch).ok().and_then(|sk| sk.constraints.iter().find(|c| c.driven && !before.contains(&c.id)).map(|c| c.id.clone()));
|
||||||
|
(added.ok_or("The driven dimension is unavailable")?, String::new())
|
||||||
|
} else {
|
||||||
|
let param = result.get("param").and_then(serde_json::Value::as_str).ok_or("This dimension has no editable value")?.to_string();
|
||||||
|
let expr = scratch.doc.param(¶m).ok_or("The dimension value is unavailable")?.expr.clone();
|
||||||
|
(param, expr)
|
||||||
|
};
|
||||||
let draft = DimensionDraft {
|
let draft = DimensionDraft {
|
||||||
baseline: app.session.doc.clone(),
|
baseline: app.session.doc.clone(),
|
||||||
sketch,
|
sketch,
|
||||||
component: app.session.active_component,
|
component: app.session.active_component,
|
||||||
path: app.session.active_occurrence_path.clone(),
|
path: app.session.active_occurrence_path.clone(),
|
||||||
pending: app.session.pending_moves.clone(),
|
pending: app.session.pending_moves.clone(),
|
||||||
entities,
|
entities: entities.to_vec(),
|
||||||
at,
|
at,
|
||||||
checked_expr: expr.clone(),
|
checked_expr: expr.clone(),
|
||||||
preview: scratch.world_state(),
|
preview: scratch.world_state(),
|
||||||
preview_doc: scratch.doc.clone(),
|
preview_doc: scratch.doc.clone(),
|
||||||
error: None,
|
error: None,
|
||||||
|
driven,
|
||||||
};
|
};
|
||||||
if !draft.preview.sketch(sketch).is_some_and(|solved| solved.report.ok()) {
|
if !draft.preview.sketch(sketch).is_some_and(|solved| solved.report.ok()) {
|
||||||
return Err("That dimension conflicts with the sketch constraints".into());
|
return Err("That dimension conflicts with the sketch constraints".into());
|
||||||
@@ -368,7 +397,9 @@ pub fn show(app: &mut SolveApp, ui: &egui::Ui, painter: &egui::Painter, proj: &P
|
|||||||
let draft_centres = draft_sketch.map(|solved| chain_centres(&solved.sketch));
|
let draft_centres = draft_sketch.map(|solved| chain_centres(&solved.sketch));
|
||||||
let mut constraints = sk.constraints.iter().map(|c| (ss, c, false)).collect::<Vec<_>>();
|
let mut constraints = sk.constraints.iter().map(|c| (ss, c, false)).collect::<Vec<_>>();
|
||||||
if let (Some(source), Some((param, _))) = (draft_sketch, draft.as_ref()) {
|
if let (Some(source), Some((param, _))) = (draft_sketch, draft.as_ref()) {
|
||||||
constraints.extend(source.sketch.constraints.iter().filter(|c| c.param.as_deref() == Some(param.as_str())).map(|c| (source, c, true)));
|
// A driven draft is keyed by its constraint id (it has no parameter).
|
||||||
|
let ours = |c: &&solvecraft_engine::sketch::Constraint| c.param.as_deref() == Some(param.as_str()) || (c.driven && c.id == *param);
|
||||||
|
constraints.extend(source.sketch.constraints.iter().filter(ours).map(|c| (source, c, true)));
|
||||||
}
|
}
|
||||||
for (source, c, is_draft) in constraints {
|
for (source, c, is_draft) in constraints {
|
||||||
if ss.sketch.view.hide_dimensions && !is_draft {
|
if ss.sketch.view.hide_dimensions && !is_draft {
|
||||||
@@ -605,6 +636,35 @@ mod tests {
|
|||||||
cancel();
|
cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Placing a dimension the sketch can't take previews it as driven: in the draft it is a
|
||||||
|
/// driven constraint with no parameter, labelled in parentheses (#320).
|
||||||
|
#[test]
|
||||||
|
fn an_over_constraining_placement_previews_a_driven_dimension() {
|
||||||
|
cancel();
|
||||||
|
let mut h = crate::scenario::Harness::new();
|
||||||
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[10,10],[30,10]],"infer":false})).unwrap();
|
||||||
|
h.app.run("sketch.dimension", json!({"entities":["l1"],"value":"20 mm"})).unwrap();
|
||||||
|
h.call("ui.view", json!({"view":"top"}));
|
||||||
|
let baseline = h.app.session.doc.to_json();
|
||||||
|
h.step(&json!({"start":"sketch.dimension"})).unwrap();
|
||||||
|
h.step(&json!({"click":{"sketch":[20,10]}})).unwrap();
|
||||||
|
h.step(&json!({"move":{"sketch":[20,20]}})).unwrap();
|
||||||
|
h.settle();
|
||||||
|
let (id, draft) = placement_preview(&h.app).expect("an over-constraining dimension still previews");
|
||||||
|
assert!(draft.driven);
|
||||||
|
let sk = &draft.preview.sketch(draft.sketch).unwrap().sketch;
|
||||||
|
let c = sk.constraints.iter().find(|c| c.id == id).unwrap();
|
||||||
|
assert!(c.driven && c.param.is_none(), "{c:?}");
|
||||||
|
assert!(matches!(c.kind, ConstraintKind::Length {value,..} if (value-20.).abs()<1e-6));
|
||||||
|
assert_eq!(label(&c.kind, None, c.driven, "mm", 2), "(20.00)");
|
||||||
|
assert!(!editing());
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), baseline);
|
||||||
|
h.step(&json!({"key":"Escape"})).unwrap();
|
||||||
|
h.step(&json!({"key":"Escape"})).unwrap();
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn placement_follows_real_pointer_without_editing_or_mutating_design() {
|
fn placement_follows_real_pointer_without_editing_or_mutating_design() {
|
||||||
cancel();
|
cancel();
|
||||||
|
|||||||
@@ -348,3 +348,33 @@ fn placing_a_two_point_dimension_picks_horizontal_vertical_or_aligned() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A dimension that would over-constrain the sketch is placed as a driven (reference) one
|
||||||
|
/// instead of failing: added with no parameter or value editor, and removed again by one
|
||||||
|
/// undo (#320).
|
||||||
|
#[test]
|
||||||
|
fn an_over_constraining_dimension_is_placed_as_a_driven_one() {
|
||||||
|
let mut h = line([20., 0.]);
|
||||||
|
h.app.run("sketch.dimension", json!({"entities":["l1"],"value":"20 mm"})).unwrap();
|
||||||
|
h.settle();
|
||||||
|
let original = h.app.session.doc.to_json();
|
||||||
|
let undo = h.app.session.undo.len();
|
||||||
|
let params = h.app.session.doc.params.len();
|
||||||
|
h.step(&json!({"start":"sketch.dimension"})).unwrap();
|
||||||
|
h.step(&json!({"click":{"world":[10,0,0]}})).unwrap();
|
||||||
|
h.step(&json!({"move":{"world":[10,6,0]}})).unwrap();
|
||||||
|
h.settle();
|
||||||
|
assert_eq!(crate::dim_view::drawn().len(), 1, "the preview is not a selectable dimension");
|
||||||
|
h.step(&json!({"click":{"world":[10,6,0]}})).unwrap();
|
||||||
|
assert!(!crate::dim_view::editing(), "a driven dimension has no value to edit");
|
||||||
|
assert_eq!(h.app.session.undo.len(), undo + 1);
|
||||||
|
assert_eq!(h.app.session.doc.params.len(), params, "a driven dimension adds no parameter");
|
||||||
|
let state = h.app.session.world_state();
|
||||||
|
let solved = state.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||||
|
assert!(solved.report.ok());
|
||||||
|
let dims: Vec<_> = solved.sketch.constraints.iter().filter(|c| c.kind.is_dimension()).collect();
|
||||||
|
assert_eq!(dims.len(), 2);
|
||||||
|
assert_eq!(dims.iter().filter(|c| c.driven && c.param.is_none()).count(), 1, "{dims:?}");
|
||||||
|
h.app.run("edit.undo", json!({})).unwrap();
|
||||||
|
assert_eq!(h.app.session.doc.to_json(), original);
|
||||||
|
}
|
||||||
@@ -112,6 +112,10 @@ Between two points, or on a line, that don't line up with an axis, where you pla
|
|||||||
dimension picks what it measures: across from them, the distance between them; above or below
|
dimension picks what it measures: across from them, the distance between them; above or below
|
||||||
them, the horizontal distance; beside them, the vertical distance.
|
them, the horizontal distance; beside them, the vertical distance.
|
||||||
|
|
||||||
|
A dimension the sketch can't take, because the geometry is already fixed by other constraints
|
||||||
|
and dimensions, is added as a driven (reference) dimension instead: it is shown in
|
||||||
|
parentheses, measures the sketch, and has no value to edit.
|
||||||
|
|
||||||
For **Coincident**, pick two points, a point and a curve (in either order), or two
|
For **Coincident**, pick two points, a point and a curve (in either order), or two
|
||||||
distinct straight lines. Two lines join their closest pair of endpoints; the first
|
distinct straight lines. Two lines join their closest pair of endpoints; the first
|
||||||
picked line stays in place when the existing constraints allow it. Pick endpoints
|
picked line stays in place when the existing constraints allow it. Pick endpoints
|
||||||
|
|||||||
Reference in new issue
Block a user