forked from 1b-agent/opencadstudio-upstream
feat(offset): classic distance-first flow with a repeat loop
OFFSET now opens with "Specify offset distance or [Through] <last>": a typed value locks the distance, bare Enter / Space accepts the last one (also offered as a prompt button), and T switches to through mode (#418). After each offset the command loops back to the object pick at the same distance until Enter / Esc finishes — Space acts as Enter throughout, where the old flow cancelled. The pick-first path (#422) keeps working: pre-selected objects jump from the distance step straight to the side click. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
1 parent
73bd304089
commit
1c8d6e1978
1 file changed
+126
-45
@@ -671,21 +671,29 @@ fn lwpolyline_pts(p: &LwPolyline) -> Vec<[f32; 3]> {
|
||||
// ── Command implementation ─────────────────────────────────────────────────
|
||||
|
||||
enum Step {
|
||||
SelectObject,
|
||||
// `locked == None` is "through" mode: the offset magnitude follows the
|
||||
// cursor (perpendicular distance to the picked object). Typing a value
|
||||
// locks the magnitude; the cursor then only chooses the side.
|
||||
/// Classic first step (#418): type the offset distance, press Enter /
|
||||
/// Space to accept the last one, or choose Through mode.
|
||||
Distance,
|
||||
/// Pick the object to offset. `locked == None` is "through" mode: the
|
||||
/// magnitude follows the cursor (perpendicular distance to the object).
|
||||
SelectObject { locked: Option<f64> },
|
||||
PickSide {
|
||||
/// The object(s) being offset — one from a pick, or the whole
|
||||
/// pre-selection when OFFSET starts with objects selected (#422).
|
||||
targets: Vec<EntityType>,
|
||||
locked: Option<f64>,
|
||||
/// True when the targets came from the pre-selection: the commit ends
|
||||
/// the command instead of looping back for another pick.
|
||||
from_selection: bool,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct OffsetCommand {
|
||||
step: Step,
|
||||
all_entities: Vec<EntityType>,
|
||||
/// Pre-selected offsettable objects (pick-first, #422); consumed when the
|
||||
/// distance step resolves.
|
||||
preselected: Vec<EntityType>,
|
||||
}
|
||||
|
||||
/// The entity types `compute_offset` can offset.
|
||||
@@ -705,22 +713,37 @@ pub fn is_offsettable(e: &EntityType) -> bool {
|
||||
impl OffsetCommand {
|
||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||
Self {
|
||||
step: Step::SelectObject,
|
||||
step: Step::Distance,
|
||||
all_entities,
|
||||
preselected: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Start straight at the side/distance step with the pre-selected
|
||||
/// objects as targets — the pick-first flow MOVE / COPY use (#422).
|
||||
/// Pick-first flow (#422): the distance step still comes first, then the
|
||||
/// pre-selected objects go straight to the side step.
|
||||
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
|
||||
Self {
|
||||
step: Step::PickSide {
|
||||
targets,
|
||||
locked: None,
|
||||
},
|
||||
step: Step::Distance,
|
||||
all_entities,
|
||||
preselected: targets,
|
||||
}
|
||||
}
|
||||
|
||||
/// Leave the distance step with the given mode (Some = locked distance,
|
||||
/// None = through): pre-selected objects jump to the side step, otherwise
|
||||
/// the object-pick loop starts.
|
||||
fn advance_from_distance(&mut self, locked: Option<f64>) -> CmdResult {
|
||||
if self.preselected.is_empty() {
|
||||
self.step = Step::SelectObject { locked };
|
||||
} else {
|
||||
self.step = Step::PickSide {
|
||||
targets: std::mem::take(&mut self.preselected),
|
||||
locked,
|
||||
from_selection: true,
|
||||
};
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for OffsetCommand {
|
||||
@@ -729,30 +752,51 @@ impl CadCommand for OffsetCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
let n = match &self.step {
|
||||
Step::PickSide { targets, .. } if targets.len() > 1 => {
|
||||
format!(" ({} objects)", targets.len())
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
match &self.step {
|
||||
Step::SelectObject => "OFFSET Select object to offset:".into(),
|
||||
Step::PickSide {
|
||||
locked: Some(d), ..
|
||||
} => format!("OFFSET{n} Click side [distance {d:.4}, type to change]:"),
|
||||
Step::PickSide { locked: None, .. } => format!(
|
||||
"OFFSET{n} Click through point, or type a distance (last {:.4}):",
|
||||
Step::Distance => format!(
|
||||
"OFFSET Specify offset distance or [Through] <{:.4}>:",
|
||||
defaults::get_offset_dist()
|
||||
),
|
||||
Step::SelectObject { .. } => {
|
||||
"OFFSET Select object to offset (Enter to finish):".into()
|
||||
}
|
||||
Step::PickSide { targets, locked, .. } => {
|
||||
let n = if targets.len() > 1 {
|
||||
format!(" ({} objects)", targets.len())
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
match locked {
|
||||
Some(d) => format!("OFFSET{n} Click side [distance {d:.4}]:"),
|
||||
None => format!("OFFSET{n} Click through point:"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
match &self.step {
|
||||
Step::Distance => vec![
|
||||
crate::command::CmdOption::new("Through", "T"),
|
||||
crate::command::CmdOption::enter(&format!(
|
||||
"{:.4}",
|
||||
defaults::get_offset_dist()
|
||||
)),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
matches!(self.step, Step::SelectObject)
|
||||
matches!(self.step, Step::SelectObject { .. })
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
||||
if handle.is_null() || !matches!(self.step, Step::SelectObject) {
|
||||
let locked = match &self.step {
|
||||
Step::SelectObject { locked } => *locked,
|
||||
_ => return CmdResult::NeedPoint,
|
||||
};
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
|
||||
@@ -768,7 +812,8 @@ impl CadCommand for OffsetCommand {
|
||||
Some(e) if is_offsettable(&e) => {
|
||||
self.step = Step::PickSide {
|
||||
targets: vec![e],
|
||||
locked: None,
|
||||
locked,
|
||||
from_selection: false,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
@@ -776,21 +821,22 @@ impl CadCommand for OffsetCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// The side step accepts an optional typed magnitude — through mode by
|
||||
// default, a fixed distance once the user types one.
|
||||
// The distance step takes a typed magnitude; the side step accepts one
|
||||
// too, re-locking the distance mid-command.
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(self.step, Step::PickSide { .. })
|
||||
matches!(self.step, Step::Distance | Step::PickSide { .. })
|
||||
}
|
||||
|
||||
fn dyn_field(&self) -> crate::command::DynField {
|
||||
match self.step {
|
||||
Step::PickSide { .. } => crate::command::DynField::Scalar,
|
||||
Step::Distance | Step::PickSide { .. } => crate::command::DynField::Scalar,
|
||||
_ => crate::command::DynField::Point,
|
||||
}
|
||||
}
|
||||
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
match &self.step {
|
||||
Step::Distance => Some(defaults::get_offset_dist()),
|
||||
Step::PickSide { targets, locked, .. } => Some(locked.unwrap_or_else(|| {
|
||||
targets
|
||||
.first()
|
||||
@@ -802,23 +848,36 @@ impl CadCommand for OffsetCommand {
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if let Step::PickSide { locked, .. } = &mut self.step {
|
||||
let t = text.trim().replace(',', ".");
|
||||
if !t.is_empty() {
|
||||
let t = text.trim().replace(',', ".");
|
||||
match &mut self.step {
|
||||
Step::Distance => {
|
||||
if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") {
|
||||
return Some(self.advance_from_distance(None));
|
||||
}
|
||||
if let Ok(d) = t.parse::<f64>() {
|
||||
let d = d.abs().max(1e-9);
|
||||
defaults::set_offset_dist(d);
|
||||
*locked = Some(d);
|
||||
return Some(self.advance_from_distance(Some(d)));
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
// Stay on the side step — the click chooses which side.
|
||||
return Some(CmdResult::NeedPoint);
|
||||
Step::PickSide { locked, .. } => {
|
||||
if !t.is_empty() {
|
||||
if let Ok(d) = t.parse::<f64>() {
|
||||
let d = d.abs().max(1e-9);
|
||||
defaults::set_offset_dist(d);
|
||||
*locked = Some(d);
|
||||
}
|
||||
}
|
||||
// Stay on the side step — the click chooses which side.
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn on_hover_entity(&mut self, handle: Handle, _pt: DVec3) -> Vec<WireModel> {
|
||||
if handle.is_null() {
|
||||
if handle.is_null() || !matches!(self.step, Step::SelectObject { .. }) {
|
||||
return vec![];
|
||||
}
|
||||
if let Some(entity) = self
|
||||
@@ -840,8 +899,12 @@ impl CadCommand for OffsetCommand {
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
let (locked, targets) = match &self.step {
|
||||
Step::PickSide { locked, targets, .. } => (*locked, targets.clone()),
|
||||
let (locked, targets, from_selection) = match &self.step {
|
||||
Step::PickSide {
|
||||
locked,
|
||||
targets,
|
||||
from_selection,
|
||||
} => (*locked, targets.clone(), *from_selection),
|
||||
_ => return CmdResult::NeedPoint,
|
||||
};
|
||||
// Each target offsets by its own through-distance (or the locked
|
||||
@@ -856,10 +919,20 @@ impl CadCommand for OffsetCommand {
|
||||
news.push(new_entity);
|
||||
}
|
||||
}
|
||||
match news.len() {
|
||||
0 => CmdResult::NeedPoint,
|
||||
1 => CmdResult::CommitAndExit(news.pop().unwrap()),
|
||||
_ => CmdResult::ReplaceMany(vec![], news),
|
||||
if news.is_empty() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if from_selection {
|
||||
// Pre-selection commit ends the command in one undo step.
|
||||
return CmdResult::ReplaceMany(vec![], news);
|
||||
}
|
||||
// Classic loop (#418): commit this offset and go back to the object
|
||||
// pick at the same distance, until Enter / Esc finishes.
|
||||
self.step = Step::SelectObject { locked };
|
||||
if news.len() == 1 {
|
||||
CmdResult::CommitEntity(news.pop().unwrap())
|
||||
} else {
|
||||
CmdResult::ReplaceMany(vec![], news)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -890,7 +963,15 @@ impl CadCommand for OffsetCommand {
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
match &self.step {
|
||||
// Enter / Space on the distance step accepts the last distance —
|
||||
// the "repeat the same value with just Space" flow (#418).
|
||||
Step::Distance => {
|
||||
let d = defaults::get_offset_dist();
|
||||
self.advance_from_distance(Some(d.abs().max(1e-9)))
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
|
||||
Reference in new issue
Block a user