A clover of same-size rectangles drawn around a shared corner was refused as over-constrained. Constraints that remove no degree of freedom but still hold (zero residual) are now kept and shown dimmed as redundant. Conflicting ones are still refused. A redundant driving dimension is refused with "already determined by ...". Typed sizes become driven (reference) dimensions. Drawing no longer adds Horizontal/Vertical to a side that is already implied through two coincident corners. The solver reports redundant constraints using a Jacobian row-dependence test. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
add4a18961
commit
9ba72e118b
14 files changed
+804
-77
No files matched your search
@@ -128,7 +128,7 @@ pub fn begin_dimension(app: &mut SolveApp, entities: Vec<String>, at: Vec2) -> R
|
||||
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);
|
||||
app.set_status("The sketch already determines that dimension, 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), live: None }));
|
||||
|
||||
@@ -156,13 +156,15 @@ pub fn lines(sk: &Sketch, prev: Vec2, lp: Vec2, tol: f64) -> Option<Inference> {
|
||||
pub fn commit(app: &mut SolveApp, line: &str, end: Vec2) {
|
||||
let Some(i) = current().filter(|i| i.at.dist(end) < 1e-9) else { return };
|
||||
clear();
|
||||
let (cmd, params) = match i.kind {
|
||||
let (cmd, mut params) = match i.kind {
|
||||
Kind::Parallel => ("sketch.constraint.parallel", json!({"a": i.curve, "b": line})),
|
||||
Kind::Perpendicular => ("sketch.constraint.perpendicular", json!({"a": i.curve, "b": line})),
|
||||
Kind::Tangent => ("sketch.constraint.tangent", json!({"a": i.curve, "b": line})),
|
||||
Kind::Extension => ("sketch.constraint.coincident", json!({"a": format!("{line}.end"), "b": i.curve})),
|
||||
};
|
||||
// An inference that would over-constrain the sketch is simply not kept.
|
||||
// An inference only adds what the sketch does not already have: one that is already implied
|
||||
// (redundant) or would conflict is simply not kept.
|
||||
params["if_redundant"] = json!("refuse");
|
||||
let _ = app.session.execute(cmd, ¶ms);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
//! Rectangles of one size drawn by clicking around a shared corner (a "clover"): each one's
|
||||
//! corner snaps onto the corners already there and its typed width and height go in. None is
|
||||
//! refused as over-constraining the sketch (#687).
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::scenario::Harness;
|
||||
|
||||
fn type_value(h: &mut Harness, text: &str) {
|
||||
h.step(&json!({"key": "A", "cmd": true})).unwrap();
|
||||
h.step(&json!({"text": text})).unwrap();
|
||||
}
|
||||
|
||||
/// Draw a rectangle from `a` (clicked, snapping onto what is there) towards `b`, typing its
|
||||
/// width and height when given, else clicking `b`.
|
||||
fn rectangle(h: &mut Harness, a: [f64; 2], b: [f64; 2], typed: bool) {
|
||||
h.step(&json!({"start": "sketch.rectangle.two_point"})).unwrap();
|
||||
h.step(&json!({"click": {"world": [a[0], a[1], 0]}})).unwrap();
|
||||
h.step(&json!({"move": {"world": [b[0], b[1], 0]}})).unwrap();
|
||||
if typed {
|
||||
type_value(h, "10");
|
||||
h.step(&json!({"key": "Tab"})).unwrap();
|
||||
type_value(h, "10");
|
||||
h.step(&json!({"key": "Enter"})).unwrap();
|
||||
} else {
|
||||
h.step(&json!({"click": {"world": [b[0], b[1], 0]}})).unwrap();
|
||||
}
|
||||
h.step(&json!({"key": "Escape"})).unwrap();
|
||||
h.settle();
|
||||
}
|
||||
|
||||
const CLOVER: [([f64; 2], [f64; 2]); 4] = [([0., 0.], [-10., -10.]), ([0., 0.], [10., -10.]), ([0., 0.], [10., 10.]), ([0., 0.], [-10., 10.])];
|
||||
|
||||
#[test]
|
||||
fn a_clover_of_typed_10_by_10_rectangles_is_drawn_and_fully_constrained() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
for (i, (a, b)) in CLOVER.iter().enumerate() {
|
||||
rectangle(&mut h, *a, *b, true);
|
||||
if let Some((message, _, true)) = &h.app.status {
|
||||
panic!("rectangle {i} was refused: {message}");
|
||||
}
|
||||
let state = h.app.session.world_state();
|
||||
let solved = state.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||
assert_eq!(solved.sketch.curves.iter().filter(|c| !c.construction).count(), 4 * (i + 1), "rectangle {i} was not added");
|
||||
assert!(solved.report.ok());
|
||||
}
|
||||
let state = h.app.session.world_state();
|
||||
let solved = state.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||
assert_eq!(solved.profiles.len(), 4);
|
||||
assert!(solved.profiles.iter().all(|p| (p.area - 100.0).abs() < 1e-6), "{:?}", solved.profiles.iter().map(|p| p.area).collect::<Vec<_>>());
|
||||
assert!(solved.report.fully_constrained(), "the clover on the origin, every size typed: {:?}", solved.report);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_clover_drawn_by_clicks_snaps_every_corner_without_redundant_constraints() {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
// Clicked, the last one goes from corner to corner of the ones already there, so both its
|
||||
// clicks snap (a free click lands only as near as the pointer allows).
|
||||
let mut clover = CLOVER;
|
||||
clover[3] = ([0., 10.], [-10., 0.]);
|
||||
for (a, b) in clover {
|
||||
rectangle(&mut h, a, b, false);
|
||||
}
|
||||
assert!(h.app.status.as_ref().is_none_or(|(_, _, error)| !error), "{:?}", h.app.status);
|
||||
let state = h.app.session.world_state();
|
||||
let solved = state.sketch(h.app.session.active_sketch.unwrap()).unwrap();
|
||||
assert_eq!(solved.profiles.len(), 4);
|
||||
assert!(solved.report.ok() && solved.report.redundant.is_empty(), "{:?}", solved.report);
|
||||
// On the origin: the two column widths and the two row heights are left.
|
||||
assert_eq!(solved.report.dof, 4);
|
||||
}
|
||||
@@ -377,3 +377,7 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_typed_intent_tests.rs"]
|
||||
mod typed_intent_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "sketch_clover_ui_tests.rs"]
|
||||
mod clover_ui_tests;
|
||||
@@ -1086,6 +1086,8 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
|
||||
for (id, sp, base) in &placed {
|
||||
let Some(c) = ss.sketch.constraints.iter().find(|c| &c.id == id) else { continue };
|
||||
let failing = ss.report.failing.contains(id);
|
||||
// Redundant (already implied by the others, but holding): dimmed, not an error.
|
||||
let redundant = !failing && ss.report.redundant.contains(id);
|
||||
let selected = picked.as_deref() == Some(id.as_str())
|
||||
|| app.session.selection.iter().any(|s| matches!(s, solidcraft_engine::Sel::SketchConstraint { id: x } if x == id));
|
||||
let lit = selected || hovered.as_deref() == Some(id.as_str());
|
||||
@@ -1093,6 +1095,8 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
|
||||
tk.error
|
||||
} else if lit {
|
||||
tk.accent
|
||||
} else if redundant {
|
||||
tk.glyph_text.gamma_multiply(0.45)
|
||||
} else {
|
||||
tk.glyph_text
|
||||
};
|
||||
@@ -1114,7 +1118,11 @@ fn glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj, hover: Option<Po
|
||||
if let (Some(id), Some(h)) = (hovered, hover)
|
||||
&& let Some(c) = ss.sketch.constraints.iter().find(|c| c.id == id)
|
||||
{
|
||||
let label = format!("{} ({id})", c.kind.name());
|
||||
let label = if ss.report.redundant.contains(&id) {
|
||||
format!("{} ({id}), redundant: already implied by other constraints", c.kind.name())
|
||||
} else {
|
||||
format!("{} ({id})", c.kind.name())
|
||||
};
|
||||
let at = h + egui::vec2(12.0, 12.0);
|
||||
let g = painter.layout_no_wrap(label, egui::FontId::proportional(11.0), tk.text);
|
||||
painter.rect_filled(egui::Rect::from_min_size(at, g.size()).expand(3.0), 3.0, tk.panel);
|
||||
|
||||
Reference in new issue
Block a user