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 <noreply@anthropic.com>
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 ─────────────────────────────────────────────────
|
// ── Command implementation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
SelectObject,
|
/// Classic first step (#418): type the offset distance, press Enter /
|
||||||
// `locked == None` is "through" mode: the offset magnitude follows the
|
/// Space to accept the last one, or choose Through mode.
|
||||||
// cursor (perpendicular distance to the picked object). Typing a value
|
Distance,
|
||||||
// locks the magnitude; the cursor then only chooses the side.
|
/// 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 {
|
PickSide {
|
||||||
/// The object(s) being offset — one from a pick, or the whole
|
/// The object(s) being offset — one from a pick, or the whole
|
||||||
/// pre-selection when OFFSET starts with objects selected (#422).
|
/// pre-selection when OFFSET starts with objects selected (#422).
|
||||||
targets: Vec<EntityType>,
|
targets: Vec<EntityType>,
|
||||||
locked: Option<f64>,
|
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 {
|
pub struct OffsetCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
all_entities: Vec<EntityType>,
|
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.
|
/// The entity types `compute_offset` can offset.
|
||||||
@@ -705,22 +713,37 @@ pub fn is_offsettable(e: &EntityType) -> bool {
|
|||||||
impl OffsetCommand {
|
impl OffsetCommand {
|
||||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: Step::SelectObject,
|
step: Step::Distance,
|
||||||
all_entities,
|
all_entities,
|
||||||
|
preselected: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start straight at the side/distance step with the pre-selected
|
/// Pick-first flow (#422): the distance step still comes first, then the
|
||||||
/// objects as targets — the pick-first flow MOVE / COPY use (#422).
|
/// pre-selected objects go straight to the side step.
|
||||||
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
|
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
step: Step::PickSide {
|
step: Step::Distance,
|
||||||
targets,
|
|
||||||
locked: None,
|
|
||||||
},
|
|
||||||
all_entities,
|
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 {
|
impl CadCommand for OffsetCommand {
|
||||||
@@ -729,30 +752,51 @@ impl CadCommand for OffsetCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
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 {
|
match &self.step {
|
||||||
Step::SelectObject => "OFFSET Select object to offset:".into(),
|
Step::Distance => format!(
|
||||||
Step::PickSide {
|
"OFFSET Specify offset distance or [Through] <{:.4}>:",
|
||||||
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}):",
|
|
||||||
defaults::get_offset_dist()
|
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 {
|
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 {
|
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;
|
return CmdResult::NeedPoint;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,7 +812,8 @@ impl CadCommand for OffsetCommand {
|
|||||||
Some(e) if is_offsettable(&e) => {
|
Some(e) if is_offsettable(&e) => {
|
||||||
self.step = Step::PickSide {
|
self.step = Step::PickSide {
|
||||||
targets: vec![e],
|
targets: vec![e],
|
||||||
locked: None,
|
locked,
|
||||||
|
from_selection: false,
|
||||||
};
|
};
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
}
|
}
|
||||||
@@ -776,21 +821,22 @@ impl CadCommand for OffsetCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The side step accepts an optional typed magnitude — through mode by
|
// The distance step takes a typed magnitude; the side step accepts one
|
||||||
// default, a fixed distance once the user types one.
|
// too, re-locking the distance mid-command.
|
||||||
fn wants_text_input(&self) -> bool {
|
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 {
|
fn dyn_field(&self) -> crate::command::DynField {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::PickSide { .. } => crate::command::DynField::Scalar,
|
Step::Distance | Step::PickSide { .. } => crate::command::DynField::Scalar,
|
||||||
_ => crate::command::DynField::Point,
|
_ => crate::command::DynField::Point,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
|
Step::Distance => Some(defaults::get_offset_dist()),
|
||||||
Step::PickSide { targets, locked, .. } => Some(locked.unwrap_or_else(|| {
|
Step::PickSide { targets, locked, .. } => Some(locked.unwrap_or_else(|| {
|
||||||
targets
|
targets
|
||||||
.first()
|
.first()
|
||||||
@@ -802,23 +848,36 @@ impl CadCommand for OffsetCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if let Step::PickSide { locked, .. } = &mut self.step {
|
let t = text.trim().replace(',', ".");
|
||||||
let t = text.trim().replace(',', ".");
|
match &mut self.step {
|
||||||
if !t.is_empty() {
|
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>() {
|
if let Ok(d) = t.parse::<f64>() {
|
||||||
let d = d.abs().max(1e-9);
|
let d = d.abs().max(1e-9);
|
||||||
defaults::set_offset_dist(d);
|
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.
|
Step::PickSide { locked, .. } => {
|
||||||
return Some(CmdResult::NeedPoint);
|
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> {
|
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![];
|
return vec![];
|
||||||
}
|
}
|
||||||
if let Some(entity) = self
|
if let Some(entity) = self
|
||||||
@@ -840,8 +899,12 @@ impl CadCommand for OffsetCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
let (locked, targets) = match &self.step {
|
let (locked, targets, from_selection) = match &self.step {
|
||||||
Step::PickSide { locked, targets, .. } => (*locked, targets.clone()),
|
Step::PickSide {
|
||||||
|
locked,
|
||||||
|
targets,
|
||||||
|
from_selection,
|
||||||
|
} => (*locked, targets.clone(), *from_selection),
|
||||||
_ => return CmdResult::NeedPoint,
|
_ => return CmdResult::NeedPoint,
|
||||||
};
|
};
|
||||||
// Each target offsets by its own through-distance (or the locked
|
// Each target offsets by its own through-distance (or the locked
|
||||||
@@ -856,10 +919,20 @@ impl CadCommand for OffsetCommand {
|
|||||||
news.push(new_entity);
|
news.push(new_entity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match news.len() {
|
if news.is_empty() {
|
||||||
0 => CmdResult::NeedPoint,
|
return CmdResult::NeedPoint;
|
||||||
1 => CmdResult::CommitAndExit(news.pop().unwrap()),
|
}
|
||||||
_ => CmdResult::ReplaceMany(vec![], news),
|
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 {
|
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 {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
|
|||||||
Reference in new issue
Block a user