forked from 1b-agent/opencadstudio-upstream
feat(input): scalar entry via dynamic input
Text-input steps (OFFSET distance, POLYGON sides, radii, counts) were swallowed by the dynamic-input overlay: typed digits landed in the field buffer and Enter committed a point pick, so the value never reached on_text_input (issue #61). Now each text step shows one scalar box whose value routes to on_text_input on commit; non-numeric prompts (names, keywords) keep going to the command line so mixed text like "Bolt12" isn't split. OFFSET reworked: object first, then through-mode by default — the offset follows the cursor at the perpendicular distance to the object. Typing a number locks the magnitude (cursor then only picks the side); a value typed and clicked without Enter is applied before the point pick. The dynamic-input overlay and the command line now show the active step's prompt and hold it until the step completes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
c9c6e45793
commit
4b878b88c4
15 files changed
+405
-70
No files matched your search
+1
-4
@@ -1734,10 +1734,7 @@ impl OpenCADStudio {
|
||||
self.tabs[i].scene.document.get_entity(h).cloned()
|
||||
})
|
||||
.collect();
|
||||
let new_cmd = OffsetCommand::new(
|
||||
crate::modules::home::defaults::get_offset_dist(),
|
||||
all_entities,
|
||||
);
|
||||
let new_cmd = OffsetCommand::new(all_entities);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
@@ -27,6 +27,10 @@ pub(super) enum DynComponent {
|
||||
Distance,
|
||||
/// Angle from the last point, in degrees.
|
||||
Angle,
|
||||
/// A scalar the command reads from the command line (a count, a radius,
|
||||
/// a delta). Typed-only — it has no geometric live value derived from
|
||||
/// the cursor unless the command supplies one via `dyn_live_value`.
|
||||
Scalar,
|
||||
}
|
||||
|
||||
/// A single editable dynamic-input field. `buffer == None` means the box
|
||||
|
||||
+83
-1
@@ -2850,6 +2850,31 @@ impl OpenCADStudio {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// A scalar typed into the dynamic-input box but not
|
||||
// yet confirmed with Enter is applied before the
|
||||
// point pick — e.g. an OFFSET distance typed and then
|
||||
// clicked takes effect rather than being discarded.
|
||||
let wants_text = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.wants_text_input())
|
||||
.unwrap_or(false);
|
||||
if wants_text {
|
||||
if let Some(text) = self.tabs[i]
|
||||
.dyn_fields
|
||||
.iter()
|
||||
.find_map(|f| f.buffer.clone())
|
||||
{
|
||||
let text = super::expr_eval::eval_to_string(text.trim());
|
||||
if let Some(c) = self.tabs[i].active_cmd.as_mut() {
|
||||
c.on_text_input(&text);
|
||||
}
|
||||
for f in self.tabs[i].dyn_fields.iter_mut() {
|
||||
f.buffer = None;
|
||||
}
|
||||
self.tabs[i].dyn_active = 0;
|
||||
}
|
||||
}
|
||||
self.last_point = Some(world_pt);
|
||||
self.dyn_user_reshaped = false;
|
||||
self.sync_dyn_fields();
|
||||
@@ -7106,10 +7131,28 @@ impl OpenCADStudio {
|
||||
.as_ref()
|
||||
.map(|c| c.dyn_field())
|
||||
.unwrap_or(crate::command::DynField::Point);
|
||||
// A text-input step reads a single scalar. The overlay shows one box
|
||||
// the user types into (or, when the command supplies a live value,
|
||||
// sets by moving the cursor); on commit the host routes it to
|
||||
// `on_text_input` instead of `on_point`. A distance prompt keeps the
|
||||
// `Distance` box (so a perpendicular-distance live value reads
|
||||
// naturally); everything else uses the typed-only `Scalar` box.
|
||||
let wants_text = self
|
||||
.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.wants_text_input())
|
||||
.unwrap_or(false);
|
||||
let has_base = self.last_point.is_some();
|
||||
let default: Vec<DynComponent> = match field {
|
||||
crate::command::DynField::Distance => vec![DynComponent::Distance],
|
||||
crate::command::DynField::Angle => vec![DynComponent::Angle],
|
||||
crate::command::DynField::Scalar => vec![DynComponent::Scalar],
|
||||
// A text prompt with the default `Point` field reads free text /
|
||||
// a name / a keyword (which may itself contain digits) from the
|
||||
// command line — show no scalar box, so digits reach the command
|
||||
// line instead of being captured into a dyn buffer.
|
||||
crate::command::DynField::Point if wants_text => vec![],
|
||||
crate::command::DynField::Point if has_base => {
|
||||
vec![DynComponent::Distance, DynComponent::Angle]
|
||||
}
|
||||
@@ -7128,7 +7171,7 @@ impl OpenCADStudio {
|
||||
// the command's default so e.g. clicking the first point of LINE
|
||||
// flips a stale `[X, Y]` (from before there was a base) over to
|
||||
// the polar `[Distance, Angle]` the prompt actually wants.
|
||||
let current_is_acceptable = if self.dyn_user_reshaped {
|
||||
let current_is_acceptable = if self.dyn_user_reshaped && !wants_text {
|
||||
match field {
|
||||
crate::command::DynField::Distance => {
|
||||
matches!(current.as_slice(), [DynComponent::Distance])
|
||||
@@ -7136,6 +7179,9 @@ impl OpenCADStudio {
|
||||
crate::command::DynField::Angle => {
|
||||
matches!(current.as_slice(), [DynComponent::Angle])
|
||||
}
|
||||
crate::command::DynField::Scalar => {
|
||||
matches!(current.as_slice(), [DynComponent::Scalar])
|
||||
}
|
||||
crate::command::DynField::Point => matches!(
|
||||
current.as_slice(),
|
||||
[DynComponent::Distance, DynComponent::Angle]
|
||||
@@ -7447,6 +7493,42 @@ impl OpenCADStudio {
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A text-input step reads its single box as a string and commits via
|
||||
// `on_text_input` (a count, radius, distance) rather than resolving a
|
||||
// point. Only the typed buffer matters here — a mouse-driven live
|
||||
// value commits through the viewport click, not Enter.
|
||||
let wants_text = self
|
||||
.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.wants_text_input())
|
||||
.unwrap_or(false);
|
||||
if wants_text {
|
||||
let text = self.tabs[i]
|
||||
.dyn_fields
|
||||
.iter()
|
||||
.find_map(|f| f.buffer.clone())
|
||||
.unwrap_or_default();
|
||||
let text = super::expr_eval::eval_to_string(text.trim());
|
||||
let result = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_mut()
|
||||
.and_then(|c| c.on_text_input(&text));
|
||||
for f in self.tabs[i].dyn_fields.iter_mut() {
|
||||
f.buffer = None;
|
||||
}
|
||||
self.tabs[i].dyn_active = 0;
|
||||
self.sync_dyn_fields();
|
||||
let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt());
|
||||
if let Some(p) = prompt {
|
||||
self.command_line.push_info(&p);
|
||||
}
|
||||
self.refresh_active_cmd_preview(i);
|
||||
return Some(match result {
|
||||
Some(r) => self.apply_cmd_result(r),
|
||||
None => self.focus_cmd_input(),
|
||||
});
|
||||
}
|
||||
let pt = self.dyn_resolve_point()?;
|
||||
self.last_point = Some(pt);
|
||||
self.dyn_user_reshaped = false;
|
||||
|
||||
+42
-7
@@ -676,18 +676,45 @@ impl OpenCADStudio {
|
||||
if self.dyn_input && tab.active_cmd.is_some() && !tab.dyn_fields.is_empty() {
|
||||
let w = tab.last_cursor_world;
|
||||
let base = self.last_point;
|
||||
// A command may drive a typed scalar by mouse (e.g. a
|
||||
// perpendicular distance to a picked object); show that live
|
||||
// value in the box until the user types over it.
|
||||
let live = tab.active_cmd.as_ref().and_then(|c| c.dyn_live_value(w));
|
||||
let boxes: Vec<overlay::DynBox> = tab
|
||||
.dyn_fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(idx, f)| overlay::DynBox {
|
||||
label: dyn_component_label(f.component),
|
||||
value: dyn_component_value(f, w, base),
|
||||
active: idx == tab.dyn_active,
|
||||
locked: f.locked(),
|
||||
.map(|(idx, f)| {
|
||||
let value = match (&f.buffer, live) {
|
||||
(Some(b), _) => b.clone(),
|
||||
(None, Some(lv))
|
||||
if matches!(
|
||||
f.component,
|
||||
DynComponent::Scalar | DynComponent::Distance
|
||||
) =>
|
||||
{
|
||||
format!("{lv:.4}")
|
||||
}
|
||||
_ => dyn_component_value(f, w, base),
|
||||
};
|
||||
overlay::DynBox {
|
||||
label: dyn_component_label(f.component),
|
||||
value,
|
||||
active: idx == tab.dyn_active,
|
||||
locked: f.locked(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
Some(overlay::dynamic_input_overlay(tab.last_cursor_screen, boxes))
|
||||
let prompt = tab
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|c| c.prompt())
|
||||
.unwrap_or_default();
|
||||
Some(overlay::dynamic_input_overlay(
|
||||
tab.last_cursor_screen,
|
||||
boxes,
|
||||
prompt,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -1150,8 +1177,13 @@ impl OpenCADStudio {
|
||||
&& !tab.dyn_fields.is_empty())
|
||||
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
||||
|| self.text_inline.is_some();
|
||||
let active_prompt = tab.active_cmd.as_ref().map(|c| c.prompt());
|
||||
let command_line_overlay =
|
||||
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
|
||||
iced::widget::container(self.command_line.view(
|
||||
allow_autocomplete,
|
||||
dyn_capturing,
|
||||
active_prompt,
|
||||
))
|
||||
.width(Fill)
|
||||
.height(Fill)
|
||||
.align_x(iced::alignment::Horizontal::Center)
|
||||
@@ -3496,6 +3528,7 @@ fn dyn_component_label(c: DynComponent) -> String {
|
||||
DynComponent::Z => "Z".into(),
|
||||
DynComponent::Distance => "d".into(),
|
||||
DynComponent::Angle => "<".into(),
|
||||
DynComponent::Scalar => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3527,5 +3560,7 @@ fn dyn_component_value(
|
||||
DynComponent::Z => format!("{:.4}", b.z),
|
||||
DynComponent::Distance => format!("{:.4}", (dx * dx + dy * dy).sqrt()),
|
||||
DynComponent::Angle => format!("{:.1}", dy.atan2(dx).to_degrees().rem_euclid(360.0)),
|
||||
// Typed-only scalar — no geometric value to track when empty.
|
||||
DynComponent::Scalar => String::new(),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user