Sketch drawing: a point snapped to a line's midpoint stays there ("mid:<line>" point arguments add a MidPoint constraint)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
d729918034
commit
063d55bdb3
4 files changed
+36
-8
No files matched your search
@@ -170,3 +170,20 @@ fn smart_constrain_and_driven_toggle() {
|
||||
let r = run(&mut s, "sketch.toggle_driven", json!({"constraint": did}));
|
||||
assert_eq!(r["driven"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_from_a_line_midpoint_keeps_it_there() {
|
||||
let mut s = new_sketch();
|
||||
let l = ids(&run(&mut s, "DrawPolyline", json!({"points": [[0, 0], [20, 0]]}))["curves"])[0].clone();
|
||||
let r = run(&mut s, "DrawPolyline", json!({"points": [format!("mid:{l}"), [10, 15]]}));
|
||||
let si = run(&mut s, "sketch.inspect", json!({}));
|
||||
assert!(si["constraints"].as_array().unwrap().iter().any(|c| c["name"] == "MidPoint"), "{si}");
|
||||
// Lengthen the first line: the second one's start follows the middle.
|
||||
run(&mut s, "sketch.move_point", json!({"point": format!("{l}.end"), "to": [40, 0]}));
|
||||
let sk = sketch(&s);
|
||||
let n = ids(&r["curves"])[0].clone();
|
||||
let st = sk.resolve_point(&format!("{n}.start")).and_then(|i| sk.point(i)).unwrap();
|
||||
let a = sk.resolve_point(&format!("{l}.start")).and_then(|i| sk.point(i)).unwrap();
|
||||
let b = sk.resolve_point(&format!("{l}.end")).and_then(|i| sk.point(i)).unwrap();
|
||||
assert!(st.dist((a + b) * 0.5) < 1e-7, "{st:?}");
|
||||
}
|
||||
@@ -328,15 +328,16 @@ fn auto_project(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
|
||||
/// Point arguments that snap to model geometry while drawing, rewritten to sketch point ids:
|
||||
/// `{"vertex": [x,y,z]}` projects the vertex (a linked point); `{"on_edge": [x,y,z]}` projects
|
||||
/// the edge and makes a free point on it (coincident with the projection). Everything else is
|
||||
/// left as it is. The links are added to the active sketch in the session's document.
|
||||
/// the edge and makes a free point on it (coincident with the projection); `"mid:<line>"`
|
||||
/// makes a point held at the line's midpoint. Everything else is left as it is. The links are added to the active sketch in the session's document.
|
||||
pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value> {
|
||||
let Some(id) = s.active_sketch else { return Ok(p.clone()) };
|
||||
let mut out = p.clone();
|
||||
let mut doc: Option<(Document, Sketch)> = None;
|
||||
let fix = |v: &mut Value, s: &Session, doc: &mut Option<(Document, Sketch)>| -> Result<()> {
|
||||
let (vertex, edge) = (v.get("vertex").and_then(vec3), v.get("on_edge").and_then(vec3));
|
||||
if vertex.is_none() && edge.is_none() {
|
||||
let mid = v.as_str().and_then(|x| x.strip_prefix("mid:")).map(str::to_string);
|
||||
if vertex.is_none() && edge.is_none() && mid.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
if doc.is_none() {
|
||||
@@ -347,7 +348,14 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
||||
}
|
||||
let Some((d, sk)) = doc.as_mut() else { return Ok(()) };
|
||||
let body = str_(v, "body").unwrap_or("").to_string();
|
||||
let pid = if let Some(at) = vertex {
|
||||
let pid = if let Some(line) = mid {
|
||||
// "mid:<line>": a new point at the line's midpoint, kept there.
|
||||
let l = sk.curve_index(&line).ok_or_else(|| bad(cmd, format!("unknown curve `{line}`")))?;
|
||||
let Some(solvecraft_sketch::Shape::Line { a, b }) = sk.shape(l) else { return Err(bad(cmd, format!("`{line}` is not a line"))) };
|
||||
let pi = sk.add_point((a + b) * 0.5, None)?;
|
||||
sk.add_constraint(ConstraintKind::Midpoint { p: pi, l }, None)?;
|
||||
sk.points.get(pi).map(|q| q.id.clone()).unwrap_or_default()
|
||||
} else if let Some(at) = vertex {
|
||||
let l = add_link(s, d, id, sk, LinkKind::Project, LinkSource::Vertex { body, at }).map_err(|e| bad(cmd, format!("vertex: {e}")))?;
|
||||
let pi = sk.link_points(&l).first().copied().ok_or_else(|| bad(cmd, "vertex projects to nothing"))?;
|
||||
sk.points.get(pi).map(|q| q.id.clone()).unwrap_or_default()
|
||||
@@ -373,7 +381,7 @@ pub(super) fn snap_points(s: &mut Session, p: &Value, cmd: &str) -> Result<Value
|
||||
fix(x, s, &mut doc)?;
|
||||
}
|
||||
}
|
||||
Value::Object(_) => fix(v, s, &mut doc)?,
|
||||
Value::Object(_) | Value::String(_) => fix(v, s, &mut doc)?,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,12 +35,13 @@ pub fn refine_snap(
|
||||
sk: &solvecraft_engine::sketch::Sketch,
|
||||
plane: &solvecraft_engine::geom::Plane,
|
||||
lp: Vec2,
|
||||
) -> Option<Vec2> {
|
||||
) -> Option<(Vec2, Option<String>)> {
|
||||
// Millimetres per pixel here.
|
||||
let (a, b) = (proj.to_screen(plane.to_world(lp))?, proj.to_screen(plane.to_world(lp + Vec2::X))?);
|
||||
let px = 1.0 / (a.distance(b) as f64).max(1e-9);
|
||||
let tol = 7.0 * px;
|
||||
let mut best: Option<(f64, &'static str, Vec2)> = None;
|
||||
let mut mid_of: Option<String> = None;
|
||||
for (i, c) in sk.curves.iter().enumerate() {
|
||||
if let solvecraft_engine::sketch::CurveKind::Line { .. } = c.kind
|
||||
&& let Some(solvecraft_engine::sketch::Shape::Line { a, b }) = sk.shape(i)
|
||||
@@ -49,6 +50,7 @@ pub fn refine_snap(
|
||||
let d = m.dist(lp);
|
||||
if d < tol && best.is_none_or(|x| d < x.0) {
|
||||
best = Some((d, "Mid", m));
|
||||
mid_of = Some(c.id.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -76,7 +78,8 @@ pub fn refine_snap(
|
||||
}
|
||||
}
|
||||
SNAP.with(|s| *s.borrow_mut() = best.map(|(_, l, p)| (l, p)));
|
||||
best.map(|(_, _, p)| p)
|
||||
// A midpoint snap is a point argument the commands understand ("mid:<line>").
|
||||
best.map(|(_, l, p)| (p, if l == "Mid" { mid_of.map(|m| format!("mid:{m}")) } else { None }))
|
||||
}
|
||||
|
||||
/// A click on a constraint glyph selects it (true: the click was used).
|
||||
|
||||
@@ -799,7 +799,7 @@ pub fn sketch_point_at(app: &SolveApp, proj: &Proj, pos: Pos2) -> Option<(Vec2,
|
||||
let (minor, _) = grid_step(app.cam.half_height());
|
||||
let step = minor / 10.0;
|
||||
if let Some(p) = crate::sketch_tools::refine_snap(app, proj, &ss.sketch, &ss.plane, lp) {
|
||||
return Some((p, None));
|
||||
return Some(p);
|
||||
}
|
||||
Some((Vec2::new((lp.x / step).round() * step, (lp.y / step).round() * step), None))
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user