diff --git a/crates/engine/src/cmd/modify.rs b/crates/engine/src/cmd/modify.rs index 302b024..b564e76 100644 --- a/crates/engine/src/cmd/modify.rs +++ b/crates/engine/src/cmd/modify.rs @@ -1937,6 +1937,7 @@ enum OffsetAsk { struct OffsetM { dist: Option, + distance_first: Option, through: bool, picked: Option, ask: Option, @@ -1949,7 +1950,17 @@ struct OffsetM { impl OffsetM { fn new(s: &Session) -> Self { let _ = s; - OffsetM { dist: None, through: false, picked: None, ask: None, multiple: false, undo: Vec::new() } + OffsetM { dist: None, distance_first: None, through: false, picked: None, ask: None, multiple: false, undo: Vec::new() } + } + + fn set_distance(&mut self, s: &mut Session, distance: f64) -> Result<()> { + if !(distance.is_finite() && distance > 0.0) { + return Err(EngineError::Other("Requires a positive distance.".into())); + } + s.doc_mut()?.header.set_f64("OFFSETDIST", distance); + self.dist = Some(distance); + self.distance_first = None; + Ok(()) } } @@ -1993,6 +2004,12 @@ impl Interactive for OffsetM { None => {} } let last = s.doc().map(|d| d.header.f64("OFFSETDIST", -1.0)).unwrap_or(-1.0); + if self.dist.is_none() + && !self.through + && let Some(base) = self.distance_first + { + return Prompt::new("Specify second point", Accept::POINT).base(base); + } match (self.dist.is_some() || self.through, self.picked) { (false, _) => Prompt::new("Specify offset distance", Accept::POINT_OR_NUMBER).kw(&["Through", "Erase", "Layer"]).default(if last < 0.0 { "Through".to_string() @@ -2037,14 +2054,26 @@ impl Interactive for OffsetM { if self.dist.is_none() && !self.through { match i { Input::Text(t) => { - let d = number(&t).filter(|d| *d > 0.0).ok_or_else(|| EngineError::Other("Requires a positive distance.".into()))?; - self.dist = Some(d); - s.doc_mut()?.header.set_f64("OFFSETDIST", d); + let d = number(&t).ok_or_else(|| EngineError::Other("Requires a positive distance.".into()))?; + self.set_distance(s, d)?; + } + Input::Point(p) => { + if !p.is_finite() { + return Err(EngineError::Other("Invalid point.".into())); + } + match self.distance_first { + Some(base) => self.set_distance(s, base.dist(p))?, + None => self.distance_first = Some(p), + } + } + Input::Keyword(k) if k == "Through" => { + self.through = true; + self.distance_first = None; } - Input::Keyword(k) if k == "Through" => self.through = true, Input::Keyword(k) if k == "Erase" => self.ask = Some(OffsetAsk::Erase), Input::Keyword(k) if k == "Layer" => self.ask = Some(OffsetAsk::Layer), Input::Enter => { + self.distance_first = None; let last = s.doc()?.header.f64("OFFSETDIST", -1.0); if last > 0.0 { self.dist = Some(last); @@ -2138,6 +2167,11 @@ impl Interactive for OffsetM { } } fn preview(&self, s: &Session, c: Vec2) -> Vec { + if let Some(base) = self.distance_first + && c.is_finite() + { + return vec![line(base, c)]; + } let (Some(h), Ok(d)) = (self.picked, s.doc()) else { return Vec::new() }; let Some(e) = d.entity(h) else { return Vec::new() }; let dist = match self.dist { diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 43852f5..6f06586 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -533,7 +533,7 @@ fn draw_tracking(p: &egui::Painter, xf: &Xf, acquired: &[snap::tracking::Acquire /// `Session::snap_cursor`: object snaps, acquiring tracking points, grid, tracking paths, ortho). fn effective_point(app: &mut CadApp, raw: Vec2, xf: &Xf, ctx: &egui::Context) -> Vec2 { let prompt = app.session.current_prompt(); - let wants_point = prompt.as_ref().is_some_and(|p| p.accept.point && !p.accept.select); + let wants_point = prompt.as_ref().is_some_and(cadcraft_engine::pointmod::accepts); let base = prompt.as_ref().and_then(|p| p.base); let deferred = prompt.as_ref().is_some_and(|p| p.deferred); app.canvas.snap = None; @@ -1106,7 +1106,7 @@ fn override_cursor(app: &mut CadApp, raw: Vec2, xf: &Xf) { let ap = app.session.settings.aperture / xf.scale; let cycle = snap_cycle(app, raw, ap); let prompt = app.session.current_prompt(); - let wants_point = prompt.as_ref().is_some_and(|p| p.accept.point && !p.accept.select); + let wants_point = prompt.as_ref().is_some_and(cadcraft_engine::pointmod::accepts); if (over.is_none() && cycle.is_none()) || !wants_point || app.session.pending_window.is_some() { return; } diff --git a/crates/ui-egui/src/cmdline.rs b/crates/ui-egui/src/cmdline.rs index a7e9a35..65f5a9c 100644 --- a/crates/ui-egui/src/cmdline.rs +++ b/crates/ui-egui/src/cmdline.rs @@ -44,7 +44,7 @@ pub fn dyn_frame(app: &CadApp) -> Option { if !st.dynmode || app.session.pending_window.is_some() { return None; } - let p = app.session.current_prompt().filter(|p| p.accept.point && !p.accept.select)?; + let p = app.session.current_prompt().filter(cadcraft_engine::pointmod::accepts)?; Some(crate::dyninput::Frame { base: p.base, cartesian: st.dynpi_cartesian, absolute: st.dynpi_absolute }) }