feat(offset): start from the pre-selected objects
OFFSET now follows the pick-first flow of MOVE / COPY (#422): with offsettable objects already selected it skips the pick step and goes straight to the distance / side step, offsetting every selected object toward the clicked side — each through the cursor point in through mode, all by the typed magnitude once one is locked. Preview covers every target and a multi-object result commits as one undo step; with nothing selected the command gathers a pick exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
554921c66f
commit
33864bace6
2 changed files with 96 additions and 45 deletions
|
|
@ -943,7 +943,7 @@ impl OpenCADStudio {
|
||||||
}
|
}
|
||||||
|
|
||||||
"OFFSET" => {
|
"OFFSET" => {
|
||||||
use crate::modules::draw::modify::offset::OffsetCommand;
|
use crate::modules::draw::modify::offset::{is_offsettable, OffsetCommand};
|
||||||
let all_entities: Vec<_> = self.tabs[i]
|
let all_entities: Vec<_> = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.entity_wires()
|
.entity_wires()
|
||||||
|
|
@ -953,7 +953,20 @@ impl OpenCADStudio {
|
||||||
self.tabs[i].scene.document.get_entity(h).cloned()
|
self.tabs[i].scene.document.get_entity(h).cloned()
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
let new_cmd = OffsetCommand::new(all_entities);
|
// Pick-first (#422): with offsettable objects already selected,
|
||||||
|
// skip the pick step and go straight to distance / side.
|
||||||
|
let preselected: Vec<_> = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.selected_entities()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(_, e)| e.clone())
|
||||||
|
.filter(is_offsettable)
|
||||||
|
.collect();
|
||||||
|
let new_cmd = if preselected.is_empty() {
|
||||||
|
OffsetCommand::new(all_entities)
|
||||||
|
} else {
|
||||||
|
OffsetCommand::with_selection(all_entities, preselected)
|
||||||
|
};
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -676,9 +676,9 @@ enum Step {
|
||||||
// cursor (perpendicular distance to the picked object). Typing a value
|
// cursor (perpendicular distance to the picked object). Typing a value
|
||||||
// locks the magnitude; the cursor then only chooses the side.
|
// locks the magnitude; the cursor then only chooses the side.
|
||||||
PickSide {
|
PickSide {
|
||||||
#[allow(dead_code)]
|
/// The object(s) being offset — one from a pick, or the whole
|
||||||
handle: Handle,
|
/// pre-selection when OFFSET starts with objects selected (#422).
|
||||||
entity: EntityType,
|
targets: Vec<EntityType>,
|
||||||
locked: Option<f64>,
|
locked: Option<f64>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -688,6 +688,20 @@ pub struct OffsetCommand {
|
||||||
all_entities: Vec<EntityType>,
|
all_entities: Vec<EntityType>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The entity types `compute_offset` can offset.
|
||||||
|
pub fn is_offsettable(e: &EntityType) -> bool {
|
||||||
|
matches!(
|
||||||
|
e,
|
||||||
|
EntityType::Line(_)
|
||||||
|
| EntityType::Circle(_)
|
||||||
|
| EntityType::Arc(_)
|
||||||
|
| EntityType::LwPolyline(_)
|
||||||
|
| EntityType::Ellipse(_)
|
||||||
|
| EntityType::Spline(_)
|
||||||
|
| EntityType::XLine(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
impl OffsetCommand {
|
impl OffsetCommand {
|
||||||
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
pub fn new(all_entities: Vec<EntityType>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
|
|
@ -695,6 +709,18 @@ impl OffsetCommand {
|
||||||
all_entities,
|
all_entities,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Start straight at the side/distance step with the pre-selected
|
||||||
|
/// objects as targets — the pick-first flow MOVE / COPY use (#422).
|
||||||
|
pub fn with_selection(all_entities: Vec<EntityType>, targets: Vec<EntityType>) -> Self {
|
||||||
|
Self {
|
||||||
|
step: Step::PickSide {
|
||||||
|
targets,
|
||||||
|
locked: None,
|
||||||
|
},
|
||||||
|
all_entities,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CadCommand for OffsetCommand {
|
impl CadCommand for OffsetCommand {
|
||||||
|
|
@ -703,13 +729,19 @@ 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::SelectObject => "OFFSET Select object to offset:".into(),
|
||||||
Step::PickSide {
|
Step::PickSide {
|
||||||
locked: Some(d), ..
|
locked: Some(d), ..
|
||||||
} => format!("OFFSET Click side [distance {d:.4}, type to change]:"),
|
} => format!("OFFSET{n} Click side [distance {d:.4}, type to change]:"),
|
||||||
Step::PickSide { locked: None, .. } => format!(
|
Step::PickSide { locked: None, .. } => format!(
|
||||||
"OFFSET Click through point, or type a distance (last {:.4}):",
|
"OFFSET{n} Click through point, or type a distance (last {:.4}):",
|
||||||
defaults::get_offset_dist()
|
defaults::get_offset_dist()
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
@ -733,16 +765,9 @@ impl CadCommand for OffsetCommand {
|
||||||
// Accept every type compute_offset can offset — including XLine (#296),
|
// Accept every type compute_offset can offset — including XLine (#296),
|
||||||
// and Ellipse/Spline whose offset functions existed but weren't reachable.
|
// and Ellipse/Spline whose offset functions existed but weren't reachable.
|
||||||
match entity {
|
match entity {
|
||||||
Some(e @ EntityType::Line(_))
|
Some(e) if is_offsettable(&e) => {
|
||||||
| Some(e @ EntityType::Circle(_))
|
|
||||||
| Some(e @ EntityType::Arc(_))
|
|
||||||
| Some(e @ EntityType::LwPolyline(_))
|
|
||||||
| Some(e @ EntityType::Ellipse(_))
|
|
||||||
| Some(e @ EntityType::Spline(_))
|
|
||||||
| Some(e @ EntityType::XLine(_)) => {
|
|
||||||
self.step = Step::PickSide {
|
self.step = Step::PickSide {
|
||||||
handle,
|
targets: vec![e],
|
||||||
entity: e,
|
|
||||||
locked: None,
|
locked: None,
|
||||||
};
|
};
|
||||||
CmdResult::NeedPoint
|
CmdResult::NeedPoint
|
||||||
|
|
@ -766,9 +791,12 @@ impl CadCommand for OffsetCommand {
|
||||||
|
|
||||||
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::PickSide { entity, locked, .. } => {
|
Step::PickSide { targets, locked, .. } => Some(locked.unwrap_or_else(|| {
|
||||||
Some(locked.unwrap_or_else(|| perp_distance(entity, cursor.as_vec3())))
|
targets
|
||||||
}
|
.first()
|
||||||
|
.map(|e| perp_distance(e, cursor.as_vec3()))
|
||||||
|
.unwrap_or(0.0)
|
||||||
|
})),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -812,43 +840,53 @@ impl CadCommand for OffsetCommand {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||||
let (locked, entity) = match &self.step {
|
let (locked, targets) = match &self.step {
|
||||||
Step::PickSide { locked, entity, .. } => (*locked, entity.clone()),
|
Step::PickSide { locked, targets, .. } => (*locked, targets.clone()),
|
||||||
_ => return CmdResult::NeedPoint,
|
_ => return CmdResult::NeedPoint,
|
||||||
};
|
};
|
||||||
let mag = locked.unwrap_or_else(|| perp_distance(&entity, pt.as_vec3()));
|
// Each target offsets by its own through-distance (or the locked
|
||||||
|
// magnitude), toward the clicked side.
|
||||||
|
let mut news: Vec<EntityType> = Vec::new();
|
||||||
|
for entity in &targets {
|
||||||
|
let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3()));
|
||||||
if mag < 1e-9 {
|
if mag < 1e-9 {
|
||||||
return CmdResult::NeedPoint;
|
continue;
|
||||||
}
|
}
|
||||||
|
if let Some(new_entity) = compute_offset(entity, mag, pt.as_vec3()) {
|
||||||
match compute_offset(&entity, mag, pt.as_vec3()) {
|
news.push(new_entity);
|
||||||
Some(new_entity) => CmdResult::CommitAndExit(new_entity),
|
}
|
||||||
None => CmdResult::NeedPoint,
|
}
|
||||||
|
match news.len() {
|
||||||
|
0 => CmdResult::NeedPoint,
|
||||||
|
1 => CmdResult::CommitAndExit(news.pop().unwrap()),
|
||||||
|
_ => CmdResult::ReplaceMany(vec![], news),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
|
||||||
let (locked, entity) = match &self.step {
|
let (locked, targets) = match &self.step {
|
||||||
Step::PickSide { locked, entity, .. } => (*locked, entity.clone()),
|
Step::PickSide { locked, targets, .. } => (*locked, targets.clone()),
|
||||||
_ => return vec![],
|
_ => return vec![],
|
||||||
};
|
};
|
||||||
let mag = locked.unwrap_or_else(|| perp_distance(&entity, pt.as_vec3()));
|
let mut wires = Vec::new();
|
||||||
|
for (n, entity) in targets.iter().enumerate() {
|
||||||
|
let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3()));
|
||||||
if mag < 1e-9 {
|
if mag < 1e-9 {
|
||||||
return vec![];
|
continue;
|
||||||
}
|
}
|
||||||
|
if let Some(result) = compute_offset(entity, mag, pt.as_vec3()) {
|
||||||
if let Some(result) = compute_offset(&entity, mag, pt.as_vec3()) {
|
|
||||||
let pts = entity_wire_pts(&result);
|
let pts = entity_wire_pts(&result);
|
||||||
if !pts.is_empty() {
|
if !pts.is_empty() {
|
||||||
return vec![WireModel::solid(
|
wires.push(WireModel::solid(
|
||||||
"offset_preview".into(),
|
format!("offset_preview_{n}"),
|
||||||
pts,
|
pts,
|
||||||
WireModel::CYAN,
|
WireModel::CYAN,
|
||||||
false,
|
false,
|
||||||
)];
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vec![]
|
}
|
||||||
|
wires
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue