Thanks @DuBearte.
This commit is contained in:
1 parent
6422ef84d5
commit
33cd031701
3 files changed
+100
-3
No files matched your search
@@ -220,7 +220,9 @@ fn live(app: &SolveApp, t: &Tool, e: Vec2, px: f64) -> Vec<(f64, Vec2)> {
|
|||||||
}
|
}
|
||||||
("sketch.arc.center_point", 2) => {
|
("sketch.arc.center_point", 2) => {
|
||||||
let a0 = (a - first).angle();
|
let a0 = (a - first).angle();
|
||||||
vec![(((e - first).angle() - a0).rem_euclid(std::f64::consts::TAU), e)]
|
// Out along the radius, clear of the cursor: a box on it would take the hover and
|
||||||
|
// the click meant for a point there (#2201).
|
||||||
|
vec![(((e - first).angle() - a0).rem_euclid(std::f64::consts::TAU), e + dir_or_x(e - first) * (2.5 * off))]
|
||||||
}
|
}
|
||||||
("sketch.conic", 2) => {
|
("sketch.conic", 2) => {
|
||||||
// `rho` is the curve's fullness, not a distance: the conic passes through
|
// `rho` is the curve's fullness, not a distance: the conic passes through
|
||||||
@@ -234,7 +236,9 @@ fn live(app: &SolveApp, t: &Tool, e: Vec2, px: f64) -> Vec<(f64, Vec2)> {
|
|||||||
}
|
}
|
||||||
("sketch.slot.center_to_center" | "sketch.slot.overall", 2) => {
|
("sketch.slot.center_to_center" | "sketch.slot.overall", 2) => {
|
||||||
let dir = dir_or_x(a - first);
|
let dir = dir_or_x(a - first);
|
||||||
vec![(2.0 * dir.cross(e - first).abs(), e)]
|
let side = if dir.cross(e - first) < 0.0 { -1.0 } else { 1.0 };
|
||||||
|
// Beyond the cursor, away from the centre line, as for the arc (#2201).
|
||||||
|
vec![(2.0 * dir.cross(e - first).abs(), e + dir.perp() * (side * 2.5 * off))]
|
||||||
}
|
}
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
}
|
}
|
||||||
@@ -402,6 +406,7 @@ fn boxes(app: &SolveApp, ui: &mut egui::Ui, proj: &Proj, t: &mut Tool) -> Option
|
|||||||
}
|
}
|
||||||
let mut completed = false;
|
let mut completed = false;
|
||||||
let mut any_focused = false;
|
let mut any_focused = false;
|
||||||
|
let view = app.viewport.layout.as_ref().map(|l| l.drawable).filter(|r| r.is_finite() && r.is_positive());
|
||||||
for (i, b) in t.dims.iter_mut().enumerate() {
|
for (i, b) in t.dims.iter_mut().enumerate() {
|
||||||
let Some((v, at)) = lv.get(i).copied() else { continue };
|
let Some((v, at)) = lv.get(i).copied() else { continue };
|
||||||
if !b.locked {
|
if !b.locked {
|
||||||
@@ -417,7 +422,7 @@ fn boxes(app: &SolveApp, ui: &mut egui::Ui, proj: &Proj, t: &mut Tool) -> Option
|
|||||||
s.cursor.set_char_range(Some(egui::text_selection::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
|
s.cursor.set_char_range(Some(egui::text_selection::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
|
||||||
s.store(ui.ctx(), id);
|
s.store(ui.ctx(), id);
|
||||||
}
|
}
|
||||||
let pos: Pos2 = sp + vec2(-34.0, -11.0);
|
let pos = in_view(sp + vec2(-34.0, -11.0), view, ui.ctx().pointer_latest_pos());
|
||||||
crate::frame::area(egui::Id::new(("sc_dim_area", i))).fixed_pos(pos).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
|
crate::frame::area(egui::Id::new(("sc_dim_area", i))).fixed_pos(pos).order(egui::Order::Foreground).show(ui.ctx(), |ui| {
|
||||||
let fill = if b.locked { tk.accent_soft } else { tk.panel };
|
let fill = if b.locked { tk.accent_soft } else { tk.panel };
|
||||||
egui::Frame::popup(ui.style()).fill(fill).inner_margin(egui::Margin::symmetric(3, 1)).show(ui, |ui| {
|
egui::Frame::popup(ui.style()).fill(fill).inner_margin(egui::Margin::symmetric(3, 1)).show(ui, |ui| {
|
||||||
@@ -465,6 +470,28 @@ fn boxes(app: &SolveApp, ui: &mut egui::Ui, proj: &Proj, t: &mut Tool) -> Option
|
|||||||
(enter && (any_focused || nothing)).then_some(e)
|
(enter && (any_focused || nothing)).then_some(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// About the size of a value box, for keeping it in view.
|
||||||
|
const BOX: egui::Vec2 = egui::vec2(76.0, 24.0);
|
||||||
|
|
||||||
|
/// Where a value box whose top left would be `pos` goes so it stays in the drawing area `view`:
|
||||||
|
/// as it is when it fits; else mirrored through the pointer (a box set off the cursor, past the
|
||||||
|
/// view's edge, goes to the cursor's other side, still clear of it, #2201); else pushed inside.
|
||||||
|
fn in_view(pos: Pos2, view: Option<egui::Rect>, pointer: Option<Pos2>) -> Pos2 {
|
||||||
|
let Some(view) = view else { return pos };
|
||||||
|
let fits = |p: Pos2| view.contains_rect(egui::Rect::from_min_size(p, BOX));
|
||||||
|
if fits(pos) {
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
if let Some(c) = pointer {
|
||||||
|
let mirrored = c + (c - (pos + BOX * 0.5)) - BOX * 0.5;
|
||||||
|
if fits(mirrored) {
|
||||||
|
return mirrored;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let max = (view.max - BOX).max(view.min);
|
||||||
|
pos.clamp(view.min, max)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[path = "sketch_dims_tests.rs"]
|
#[path = "sketch_dims_tests.rs"]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
use super::{BOX, in_view};
|
||||||
use crate::scenario::Harness;
|
use crate::scenario::Harness;
|
||||||
|
|
||||||
fn drawing(command: &str) -> Harness {
|
fn drawing(command: &str) -> Harness {
|
||||||
@@ -316,3 +317,25 @@ fn a_line_end_on_a_circle_outside_the_axis_snap_stays_at_the_cursor() {
|
|||||||
let end = sk.point(b).unwrap();
|
let end = sk.point(b).unwrap();
|
||||||
assert!((end.len() - 30.0).abs() < 1e-9 && end.dist(at) < 0.5, "{end:?}");
|
assert!((end.len() - 30.0).abs() < 1e-9 && end.dist(at) < 0.5, "{end:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A value box set off the cursor stays in the drawing area (#2201): past an edge it goes to the
|
||||||
|
/// cursor's other side, still clear of it; with no room either side it is pushed inside.
|
||||||
|
#[test]
|
||||||
|
fn value_boxes_stay_in_view_and_off_the_cursor() {
|
||||||
|
let view = egui::Rect::from_min_max(egui::pos2(250.0, 130.0), egui::pos2(1650.0, 960.0));
|
||||||
|
let cursor = egui::pos2(565.0, 142.0);
|
||||||
|
// 50 px above the cursor: over the ribbon.
|
||||||
|
let above = cursor + egui::vec2(-34.0, -11.0 - 50.0);
|
||||||
|
let p = in_view(above, Some(view), Some(cursor));
|
||||||
|
let rect = egui::Rect::from_min_size(p, BOX);
|
||||||
|
assert!(view.contains_rect(rect), "{rect:?} in {view:?}");
|
||||||
|
assert!(!rect.expand(2.0).contains(cursor), "{rect:?} clear of {cursor:?}");
|
||||||
|
assert!(rect.min.y > cursor.y, "below the cursor now");
|
||||||
|
// A box that fits stays where it is; without a view nothing moves.
|
||||||
|
let inside = egui::pos2(700.0, 500.0);
|
||||||
|
assert_eq!(in_view(inside, Some(view), Some(cursor)), inside);
|
||||||
|
assert_eq!(in_view(above, None, Some(cursor)), above);
|
||||||
|
// No room on either side: inside the view all the same.
|
||||||
|
let tiny = egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(80.0, 30.0));
|
||||||
|
assert!(tiny.contains_rect(egui::Rect::from_min_size(in_view(egui::pos2(-200.0, -200.0), Some(tiny), Some(egui::pos2(40.0, 15.0))), BOX)));
|
||||||
|
}
|
||||||
@@ -182,3 +182,50 @@ fn typed_drawing_preserves_construction_and_centerline_modes_at_undo_limit() {
|
|||||||
crate::sketch_palette::set_mode(&mut h.app, 0);
|
crate::sketch_palette::set_mode(&mut h.app, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The third pick of a Center Point Arc reaches existing geometry under the pointer: the value
|
||||||
|
/// box (the arc's sweep, a slot's width) stays clear of the cursor instead of sitting on it
|
||||||
|
/// (#2201).
|
||||||
|
#[test]
|
||||||
|
fn the_arc_end_pick_snaps_to_a_point_under_the_value_box() {
|
||||||
|
for (command, picks, end) in [
|
||||||
|
("sketch.arc.center_point", [[0.0, 0.0], [10.0, 0.0]], [0.0, 10.0]),
|
||||||
|
("sketch.slot.center_to_center", [[-10.0, 0.0], [10.0, 0.0]], [0.0, 10.0]),
|
||||||
|
] {
|
||||||
|
let mut h = Harness::new();
|
||||||
|
h.app.run("sketch.create", json!({"plane":"XY"})).unwrap();
|
||||||
|
h.app.run("sketch.line", json!({"points":[[0, 0], end], "infer": false})).unwrap();
|
||||||
|
h.app.run("sketch.construction", json!({"curves":["l1"]})).unwrap();
|
||||||
|
h.call("ui.view", json!({"view":"top"}));
|
||||||
|
h.app.cam.distance = 60.0;
|
||||||
|
h.app.cam.target = solidcraft_engine::geom::Vec3::new(0.0, 5.0, 0.0);
|
||||||
|
h.settle();
|
||||||
|
h.step(&json!({"start":command})).unwrap();
|
||||||
|
for p in picks {
|
||||||
|
h.step(&json!({"click":{"world":[p[0], p[1], 0]}})).unwrap();
|
||||||
|
}
|
||||||
|
h.step(&json!({"move":{"world":[end[0], end[1], 0]}})).unwrap();
|
||||||
|
let cursor = h.at(&json!({"world":[end[0], end[1], 0]})).unwrap();
|
||||||
|
let cursor = egui::pos2(cursor[0] as f32, cursor[1] as f32);
|
||||||
|
let blocking = h.ctx.memory(|m| m.area_rect(egui::Id::new(("sc_dim_area", 0usize)))).filter(|r| r.expand(2.0).contains(cursor));
|
||||||
|
assert!(blocking.is_none(), "{command}: the value box {blocking:?} sits under the cursor {cursor:?}");
|
||||||
|
h.step(&json!({"click":{"world":[end[0], end[1], 0]}})).unwrap();
|
||||||
|
// A slot's third pick sets its width only; the arc's end takes the point it lands on.
|
||||||
|
if command != "sketch.arc.center_point" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let state = h.app.session.world_state();
|
||||||
|
let sketch = &state.sketch(h.app.session.active_sketch.unwrap()).unwrap().sketch;
|
||||||
|
let line_end = sketch.curves.iter().find(|c| c.id == "l1").and_then(|c| match c.kind {
|
||||||
|
CurveKind::Line { b, .. } => Some(b),
|
||||||
|
_ => None,
|
||||||
|
});
|
||||||
|
let snapped =
|
||||||
|
sketch.constraints.iter().any(|c| matches!(&c.kind, ConstraintKind::Coincident { p, q } if Some(*p) == line_end || Some(*q) == line_end))
|
||||||
|
|| sketch.curves.iter().filter(|c| c.id != "l1").any(|c| match c.kind {
|
||||||
|
CurveKind::Arc { a, b, .. } => Some(a) == line_end || Some(b) == line_end,
|
||||||
|
_ => false,
|
||||||
|
});
|
||||||
|
assert!(snapped, "{command}: the third pick snapped to the line's end: {:?}", sketch.constraints.iter().map(|c| &c.kind).collect::<Vec<_>>());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user