Sketch: lines snap to horizontal/vertical near the axes (with an H/V marker; the constraint is inferred), inline dimension boxes sit beside what they measure instead of on it; Esc acts once per press (ends the line chain first, then the tool)
Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
1 parent
d44a104dcc
commit
b457b6b7b6
5 files changed
+68
-12
No files matched your search
@@ -1179,7 +1179,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
if enter_applies && (enter || canvas_enter || (enter_free && nothing_focused)) {
|
||||
ok = true;
|
||||
}
|
||||
if esc {
|
||||
if esc && !app.esc_handled {
|
||||
app.esc_handled = true;
|
||||
cancel = true;
|
||||
}
|
||||
// A plane picked for a new sketch starts it right away.
|
||||
|
||||
@@ -134,6 +134,8 @@ pub struct SolveApp {
|
||||
pub preview: preview::PreviewState,
|
||||
/// The last command started interactively (id, label), for Repeat.
|
||||
pub last_command: Option<(String, String)>,
|
||||
/// Esc was already acted on this frame (by the shortcuts), so later handlers skip it.
|
||||
pub esc_handled: bool,
|
||||
pub status: Option<(String, f64, bool)>,
|
||||
pub quit_requested: bool,
|
||||
/// The application bar is the window's title bar (Windows, Linux: no OS decorations).
|
||||
@@ -170,6 +172,7 @@ impl SolveApp {
|
||||
tree: browser::TreeState::default(),
|
||||
preview: preview::PreviewState::default(),
|
||||
last_command: None,
|
||||
esc_handled: false,
|
||||
status: None,
|
||||
quit_requested: false,
|
||||
custom_titlebar: false,
|
||||
@@ -508,6 +511,7 @@ impl SolveApp {
|
||||
|
||||
/// Per-frame logic before layout.
|
||||
pub fn logic(&mut self, ctx: &egui::Context) {
|
||||
self.esc_handled = false;
|
||||
if !self.styled || theme::is_dark() != self.ui.dark {
|
||||
theme::set_dark(self.ui.dark);
|
||||
theme::apply(ctx);
|
||||
|
||||
@@ -70,7 +70,7 @@ pub fn effective(app: &SolveApp, t: &Tool, h: Vec2) -> Vec2 {
|
||||
("DrawPolyline", _) | ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 1) => {
|
||||
let v = h - a;
|
||||
let len = lk("Length").unwrap_or(v.len());
|
||||
let ang = lk("Angle").unwrap_or_else(|| v.angle());
|
||||
let ang = lk("Angle").unwrap_or_else(|| snap_axis(v.angle()));
|
||||
a + Vec2::from_angle(ang) * len
|
||||
}
|
||||
("ShapeRectangleTwoPoint", 1) => {
|
||||
@@ -105,23 +105,56 @@ pub fn effective(app: &SolveApp, t: &Tool, h: Vec2) -> Vec2 {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
let q = std::f64::consts::FRAC_PI_2;
|
||||
let k = (ang / q).round();
|
||||
if (ang - k * q).abs() < 2.5f64.to_radians() { k * q } else { ang }
|
||||
}
|
||||
|
||||
/// Is the segment exactly horizontal or vertical (an inferred constraint)?
|
||||
pub fn axis_aligned(a: Vec2, b: Vec2) -> Option<bool> {
|
||||
let d = b - a;
|
||||
if d.len() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
if d.y.abs() < 1e-9 * d.len() {
|
||||
Some(true)
|
||||
} else if d.x.abs() < 1e-9 * d.len() {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// The value each box shows while it follows the cursor, and where it sits (sketch coordinates).
|
||||
fn live(t: &Tool, e: Vec2) -> Vec<(f64, Vec2)> {
|
||||
/// `px` is the size of a screen pixel in sketch units: boxes sit a little off the segment they
|
||||
/// measure so they don't cover it.
|
||||
fn live(t: &Tool, e: Vec2, px: f64) -> Vec<(f64, Vec2)> {
|
||||
let Some(a) = t.pts.last().map(|p| p.0) else { return Vec::new() };
|
||||
let first = t.pts.first().map(|p| p.0).unwrap_or(a);
|
||||
let off = 20.0 * px;
|
||||
match (t.cmd, t.pts.len()) {
|
||||
("DrawPolyline", _) | ("ShapeSlotCenterToCenter" | "ShapeSlotOverall", 1) => {
|
||||
let v = e - a;
|
||||
vec![(v.len(), (a + e) * 0.5), (v.angle(), a + dir_or_x(v) * (v.len() * 0.2).min(12.0))]
|
||||
let d = dir_or_x(v);
|
||||
let n = d.perp();
|
||||
vec![(v.len(), (a + e) * 0.5 + n * off), (v.angle(), a + d * (v.len() * 0.3).min(60.0 * px) - n * off)]
|
||||
}
|
||||
("ShapeRectangleTwoPoint", 1) => {
|
||||
vec![((e.x - a.x).abs(), Vec2::new((a.x + e.x) * 0.5, a.y)), ((e.y - a.y).abs(), Vec2::new(e.x, (a.y + e.y) * 0.5))]
|
||||
let (sx, sy) = (sign(e.x - a.x), sign(e.y - a.y));
|
||||
vec![
|
||||
((e.x - a.x).abs(), Vec2::new((a.x + e.x) * 0.5, a.y - sy * off)),
|
||||
((e.y - a.y).abs(), Vec2::new(e.x + sx * 2.0 * off, (a.y + e.y) * 0.5)),
|
||||
]
|
||||
}
|
||||
("ShapeRectangleCenter", 1) => {
|
||||
let d = e - a;
|
||||
vec![(2.0 * d.x.abs(), Vec2::new(a.x, a.y - d.y)), (2.0 * d.y.abs(), Vec2::new(a.x + d.x, a.y))]
|
||||
let (sx, sy) = (sign(d.x), sign(d.y));
|
||||
vec![(2.0 * d.x.abs(), Vec2::new(a.x, a.y - d.y - sy * off)), (2.0 * d.y.abs(), Vec2::new(a.x + d.x + sx * 2.0 * off, a.y))]
|
||||
}
|
||||
("CircleCenterRadius", 1) => vec![(2.0 * (e - a).len(), (a + e) * 0.5)],
|
||||
("CircleCenterRadius", 1) => vec![(2.0 * (e - a).len(), (a + e) * 0.5 + dir_or_x(e - a).perp() * off)],
|
||||
("ArcCenterTwoPoint", 1) | ("ShapePolygonInscribed" | "ShapePolygonCircumscribed", 1) => {
|
||||
vec![((e - a).len(), (a + e) * 0.5), (6.0, a + (e - a) * 0.5 + (e - a).perp() * 0.3)]
|
||||
}
|
||||
@@ -183,7 +216,8 @@ pub fn show(app: &mut SolveApp, ui: &mut egui::Ui, proj: &Proj) {
|
||||
// Esc in a box ends the shape (the box had the keyboard, so the shortcut didn't see it).
|
||||
let in_box =
|
||||
app.tool.as_ref().is_some_and(|t| (0..t.dims.len()).any(|i| ui.ctx().memory(|m| m.had_focus_last_frame(egui::Id::new(("sc_dim", i))))));
|
||||
if in_box && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
if in_box && !app.esc_handled && ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
app.esc_handled = true;
|
||||
crate::tools::finish(app);
|
||||
return;
|
||||
}
|
||||
@@ -203,7 +237,8 @@ fn boxes(app: &SolveApp, ui: &mut egui::Ui, proj: &Proj, t: &mut Tool) -> Option
|
||||
let plane = st.sketch(sid)?.plane;
|
||||
let h = t.hover.as_ref().map(|x| x.0)?;
|
||||
let e = effective(app, t, h);
|
||||
let lv = live(t, e);
|
||||
let px = 2.0 * app.cam.half_height() / f64::from(proj.rect.height().max(1.0));
|
||||
let lv = live(t, e, px);
|
||||
let focus = std::mem::take(&mut t.dims_focus);
|
||||
let mut any_focused = false;
|
||||
for (i, b) in t.dims.iter_mut().enumerate() {
|
||||
|
||||
@@ -406,6 +406,7 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
}
|
||||
match k {
|
||||
Key::Escape => {
|
||||
app.esc_handled = true;
|
||||
if app.tool.is_some() {
|
||||
crate::tools::finish(app);
|
||||
} else if app.dialog.is_some() {
|
||||
|
||||
@@ -111,8 +111,8 @@ pub fn on_click(app: &mut SolveApp, proj: &Proj, pos: Pos2) {
|
||||
match tool.kind {
|
||||
Kind::Draw(_) => {
|
||||
if let Some(mut p) = sketch_point_at(app, proj, pos) {
|
||||
// Typed values win over the cursor.
|
||||
if tool.dims.iter().any(|b| b.locked) {
|
||||
// Typed values win over the cursor; lines snap to horizontal/vertical.
|
||||
if tool.dims.iter().any(|b| b.locked) || (tool.cmd == "DrawPolyline" && p.1.is_none()) {
|
||||
p = (crate::sketch_dims::effective(app, &tool, p.0), None);
|
||||
}
|
||||
app.tool = Some(tool);
|
||||
@@ -304,7 +304,22 @@ pub fn preview(app: &SolveApp, t: &Tool, painter: &egui::Painter, proj: &Proj) {
|
||||
painter.add(Shape::line(v, stroke));
|
||||
};
|
||||
match (t.cmd, pts.as_slice()) {
|
||||
("DrawPolyline", [.., last]) => line(*last, h),
|
||||
("DrawPolyline", [.., last]) => {
|
||||
line(*last, h);
|
||||
// Horizontal/vertical inference marker.
|
||||
if let Some(horizontal) = crate::sketch_dims::axis_aligned(*last, h)
|
||||
&& let Some(m) = to((*last + h) * 0.5)
|
||||
{
|
||||
let g = if horizontal { egui::vec2(0.0, -12.0) } else { egui::vec2(12.0, 0.0) };
|
||||
painter.text(
|
||||
m + g,
|
||||
egui::Align2::CENTER_CENTER,
|
||||
if horizontal { "H" } else { "V" },
|
||||
egui::FontId::proportional(10.0),
|
||||
tk.sketch_accent,
|
||||
);
|
||||
}
|
||||
}
|
||||
("ShapeRectangleTwoPoint", [a]) => {
|
||||
let (b, c) = (Vec2::new(h.x, a.y), Vec2::new(a.x, h.y));
|
||||
line(*a, b);
|
||||
|
||||
Reference in new issue
Block a user