A sketch line end on a curve within the axis snap lands where the preview shows (#1122)
A line end snapped onto a curve within the horizontal/vertical snap slides along the curve to where the axis crosses it; the preview, value boxes and click share one placement (#1121).
This commit is contained in:
1 parent
aa2ace81af
commit
ed943a74e6
3 files changed
+120
-8
No files matched your search
@@ -107,6 +107,54 @@ pub fn effective(app: &SolveApp, t: &Tool, h: Vec2) -> Vec2 {
|
||||
}
|
||||
}
|
||||
|
||||
/// The point a click places for the hover `p` (the preview draws this one): typed values win
|
||||
/// over the cursor and drop its snap; a line's free end snaps to horizontal/vertical, one on
|
||||
/// sketch geometry stays there, and one on a curve slides along it to where the axis crosses.
|
||||
pub fn placed(app: &SolveApp, t: &Tool, p: &(Vec2, Option<String>)) -> (Vec2, Option<String>) {
|
||||
let line = t.cmd == "sketch.line";
|
||||
if t.dims.iter().any(|b| b.locked) || (line && p.1.is_none()) {
|
||||
return (effective(app, t, p.0), None);
|
||||
}
|
||||
if line && let Some(q) = on_curve_axis(app, t, p) {
|
||||
return q;
|
||||
}
|
||||
p.clone()
|
||||
}
|
||||
|
||||
/// Where the preview and the value boxes put the tool's next point for the hover `p`: for a
|
||||
/// line, exactly what a click places.
|
||||
pub fn shown(app: &SolveApp, t: &Tool, p: &(Vec2, Option<String>)) -> Vec2 {
|
||||
if t.cmd == "sketch.line" { placed(app, t, p).0 } else { effective(app, t, p.0) }
|
||||
}
|
||||
|
||||
/// A line end snapped onto a curve (`on:<curve>:x,y`) within the axis snap: the point where the
|
||||
/// horizontal/vertical line from the start crosses that curve near the cursor (from a circle's
|
||||
/// centre, its quadrant point), still on the curve. None: no axis snap there, or the axis misses
|
||||
/// the curve close by (the curve snap wins as it is).
|
||||
fn on_curve_axis(app: &SolveApp, t: &Tool, p: &(Vec2, Option<String>)) -> Option<(Vec2, Option<String>)> {
|
||||
let a = t.pts.last()?.0;
|
||||
let (curve, _) = p.1.as_deref()?.strip_prefix("on:")?.rsplit_once(':')?;
|
||||
let v = p.0 - a;
|
||||
let ang = snap_axis(v.angle());
|
||||
if ang == v.angle() {
|
||||
return None;
|
||||
}
|
||||
let st = app.session.world_state();
|
||||
let sk = &st.sketch(app.session.active_sketch?)?.sketch;
|
||||
let shape = sk.shape(sk.curve_index(curve)?)?;
|
||||
let dir = Vec2::from_angle(ang);
|
||||
let horizontal = dir.y.abs() < dir.x.abs();
|
||||
let axis = solidcraft_engine::sketch::Shape::Line { a, b: a + dir };
|
||||
let tol = 1e-9 * (1.0 + v.len());
|
||||
let q = solidcraft_engine::sketch::intersections(&axis, &shape)
|
||||
.into_iter()
|
||||
// Exactly on the axis, so the command infers the horizontal/vertical constraint.
|
||||
.map(|q| if horizontal { Vec2::new(q.x, a.y) } else { Vec2::new(a.x, q.y) })
|
||||
.filter(|q| (*q - a).dot(dir) > tol && shape.dist(*q) <= tol && q.dist(p.0) <= v.len() * 0.1)
|
||||
.min_by(|x, y| x.dist(p.0).total_cmp(&y.dist(p.0)))?;
|
||||
Some((q, Some(format!("on:{curve}:{},{}", q.x, q.y))))
|
||||
}
|
||||
|
||||
/// Angles within a few degrees of horizontal or vertical snap to it (the line is drawn
|
||||
/// horizontal or vertical, and the command infers the constraint).
|
||||
pub fn snap_axis(ang: f64) -> f64 {
|
||||
@@ -325,8 +373,7 @@ fn boxes(app: &SolveApp, ui: &mut egui::Ui, proj: &Proj, t: &mut Tool) -> Option
|
||||
let sid = app.session.active_sketch?;
|
||||
let st = app.session.world_state();
|
||||
let plane = st.sketch(sid)?.plane;
|
||||
let h = t.hover.as_ref().map(|x| x.0)?;
|
||||
let e = effective(app, t, h);
|
||||
let e = shown(app, t, t.hover.as_ref()?);
|
||||
let px = 2.0 * app.cam.half_height() / f64::from(proj.rect.height().max(1.0));
|
||||
let lv = live(app, t, e, px);
|
||||
let focus = std::mem::take(&mut t.dims_focus);
|
||||
|
||||
@@ -249,3 +249,70 @@ fn rounded_rectangle_clicks_use_a_default_radius() {
|
||||
let want = w * hh - (4.0 - std::f64::consts::PI) * r * r;
|
||||
assert!((sketch.profiles[0].area - want).abs() < 0.05 * want, "area {} want about {want}", sketch.profiles[0].area);
|
||||
}
|
||||
|
||||
/// A line from a circle's centre to the circle, ending a little off the quadrant: the preview
|
||||
/// snaps to horizontal or vertical, so the click must place exactly that line, on the circle.
|
||||
#[test]
|
||||
fn a_line_end_on_a_circle_within_the_axis_snap_lands_on_the_quadrant_the_preview_shows() {
|
||||
use solidcraft_engine::geom::Vec2;
|
||||
use solidcraft_engine::sketch::{ConstraintKind, CurveKind};
|
||||
let r = 30.0_f64;
|
||||
for (deg, end, horizontal) in [(-1.5_f64, Vec2::new(r, 0.0), true), (91.8, Vec2::new(0.0, r), false), (178.0, Vec2::new(-r, 0.0), true)] {
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.circle.center", json!({"center": [0, 0], "radius": r})).unwrap();
|
||||
h.settle();
|
||||
let sid = h.app.session.active_sketch.unwrap();
|
||||
h.step(&json!({"start": "sketch.line"})).unwrap();
|
||||
h.step(&json!({"click": {"world": [0, 0, 0]}})).unwrap();
|
||||
let at = Vec2::from_angle(deg.to_radians()) * r;
|
||||
h.step(&json!({"move": {"world": [at.x, at.y, 0]}})).unwrap();
|
||||
let tool = h.app.tool.as_ref().unwrap();
|
||||
let hover = tool.hover.clone().unwrap();
|
||||
assert!(hover.1.as_deref().is_some_and(|s| s.starts_with("on:")), "{deg}: {hover:?}");
|
||||
let shown = crate::sketch_dims::shown(&h.app, tool, &hover);
|
||||
assert!(shown.dist(end) < 1e-9, "{deg}: preview at {shown:?}");
|
||||
h.step(&json!({"click": {"world": [at.x, at.y, 0]}})).unwrap();
|
||||
let st = h.app.session.world_state();
|
||||
let sk = &st.sketch(sid).unwrap().sketch;
|
||||
let (line, a, b) = sk
|
||||
.curves
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(i, c)| if let CurveKind::Line { a, b } = c.kind { Some((i, a, b)) } else { None })
|
||||
.unwrap_or_else(|| panic!("{deg}: no line, {:?}", h.app.status));
|
||||
assert!(sk.point(a).unwrap().dist(Vec2::ZERO) < 1e-9, "{deg}");
|
||||
assert!(sk.point(b).unwrap().dist(end) < 1e-9, "{deg}: placed at {:?}", sk.point(b));
|
||||
assert!(sk.constraints.iter().any(|c| matches!(c.kind, ConstraintKind::PointOnCurve { p, c: 0 } if p == b)), "{deg}: end not on the circle");
|
||||
let axis = |k: &ConstraintKind| {
|
||||
if horizontal {
|
||||
matches!(k, ConstraintKind::Horizontal { l } if *l == line)
|
||||
} else {
|
||||
matches!(k, ConstraintKind::Vertical { l } if *l == line)
|
||||
}
|
||||
};
|
||||
assert!(sk.constraints.iter().any(|c| axis(&c.kind)), "{deg}: axis constraint missing");
|
||||
}
|
||||
}
|
||||
|
||||
/// Outside the axis snap a line end on a circle stays where the cursor meets the circle.
|
||||
#[test]
|
||||
fn a_line_end_on_a_circle_outside_the_axis_snap_stays_at_the_cursor() {
|
||||
use solidcraft_engine::geom::Vec2;
|
||||
use solidcraft_engine::sketch::CurveKind;
|
||||
let mut h = Harness::new();
|
||||
h.app.run("sketch.create", json!({"plane": "XY"})).unwrap();
|
||||
h.app.run("sketch.circle.center", json!({"center": [0, 0], "radius": 30})).unwrap();
|
||||
h.settle();
|
||||
let sid = h.app.session.active_sketch.unwrap();
|
||||
h.step(&json!({"start": "sketch.line"})).unwrap();
|
||||
h.step(&json!({"click": {"world": [0, 0, 0]}})).unwrap();
|
||||
let at = Vec2::from_angle(20f64.to_radians()) * 30.0;
|
||||
h.step(&json!({"move": {"world": [at.x, at.y, 0]}})).unwrap();
|
||||
h.step(&json!({"click": {"world": [at.x, at.y, 0]}})).unwrap();
|
||||
let st = h.app.session.world_state();
|
||||
let sk = &st.sketch(sid).unwrap().sketch;
|
||||
let b = sk.curves.iter().find_map(|c| if let CurveKind::Line { b, .. } = c.kind { Some(b) } else { None }).unwrap();
|
||||
let end = sk.point(b).unwrap();
|
||||
assert!((end.len() - 30.0).abs() < 1e-9 && end.dist(at) < 0.5, "{end:?}");
|
||||
}
|
||||
@@ -150,11 +150,9 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
}
|
||||
match tool.kind {
|
||||
Kind::Draw(n) => {
|
||||
if let Some(mut p) = sketch_point_at(app, proj, pos) {
|
||||
if let Some(p) = sketch_point_at(app, proj, pos) {
|
||||
// Typed values win over the cursor; lines snap to horizontal/vertical.
|
||||
if tool.dims.iter().any(|b| b.locked) || (tool.cmd == "sketch.line" && p.1.is_none()) {
|
||||
p = (crate::sketch_dims::effective(app, &tool, p.0), None);
|
||||
}
|
||||
let p = crate::sketch_dims::placed(app, &tool, &p);
|
||||
// A repeated initial point supplies no extent. Keep waiting rather than
|
||||
// attempting a degenerate shape; explicit typed values still validate.
|
||||
if n != 1 && tool.pts.len() == 1 && tool.pts.first().is_some_and(|first| first.0 == p.0) && !tool.dims.iter().any(|b| b.locked) {
|
||||
@@ -691,8 +689,8 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
tk.text_dim,
|
||||
);
|
||||
}
|
||||
let Some((h, _)) = t.hover.clone() else { return };
|
||||
let h = crate::sketch_dims::effective(app, t, h);
|
||||
let Some(hover) = t.hover.as_ref() else { return };
|
||||
let h = crate::sketch_dims::shown(app, t, hover);
|
||||
if let Some(corners) = polygon_outline(t, h, crate::sketch_dims::locked_value(app, t, "Sides").unwrap_or(6.0)) {
|
||||
let outline: Vec<_> = corners.iter().map(|c| ss.plane.to_world(*c)).collect();
|
||||
paint_sketch_path(&outline, crate::sketch_palette::mode(app), painter, proj, stroke);
|
||||
|
||||
Reference in new issue
Block a user