forked from 1b-agent/opencadstudio-upstream
Convert: turn associative dimensions into dimensional constraints
The command deleted the dimension and left a literal constraint behind. The reference keeps the dimension, drives it with a new parameter and gathers only associative dimensions, saying how many objects it dropped. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
133bfddba9
commit
6d36356529
4 files changed
+296
-88
No files matched your search
@@ -432,11 +432,62 @@ impl OpenCADStudio {
|
||||
self.feed_command_consumed(input).0
|
||||
}
|
||||
|
||||
/// Keeps only associative dimensions in a completed selection when the
|
||||
/// active command asks for that, deselecting the rest and reporting how
|
||||
/// many objects were dropped.
|
||||
fn filter_associative_dimension_selection(&mut self, input: StepInput) -> StepInput {
|
||||
let StepInput::SelectionComplete(handles) = input else {
|
||||
return input;
|
||||
};
|
||||
let i = self.active_tab;
|
||||
if !self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.selection_keeps_associative_dimensions())
|
||||
{
|
||||
return StepInput::SelectionComplete(handles);
|
||||
}
|
||||
let (kept, dropped): (Vec<Handle>, Vec<Handle>) = handles.into_iter().partition(|handle| {
|
||||
// A dimension that already drives a constraint is not one to
|
||||
// convert either.
|
||||
crate::scene::parametric_constraints::dynamic_dimension_constraint(
|
||||
&self.tabs[i].scene.parametric_constraints,
|
||||
*handle,
|
||||
)
|
||||
.is_none()
|
||||
&& crate::scene::dimension_assoc::constraint_from_associative_dimension(
|
||||
&self.tabs[i].scene.document,
|
||||
*handle,
|
||||
)
|
||||
.is_some()
|
||||
});
|
||||
for handle in &dropped {
|
||||
self.tabs[i].scene.deselect_entity(*handle);
|
||||
}
|
||||
if !dropped.is_empty() {
|
||||
let count = dropped.len();
|
||||
self.command_line.push_info(
|
||||
if count == 1 {
|
||||
crate::t!("1 was filtered out.")
|
||||
} else {
|
||||
crate::tf!("{count} were filtered out.")
|
||||
}
|
||||
.as_ref(),
|
||||
);
|
||||
self.refresh_properties();
|
||||
}
|
||||
StepInput::SelectionComplete(kept)
|
||||
}
|
||||
|
||||
/// `feed_command`, also reporting whether the step actually took the input.
|
||||
/// Only a `Text` token can come back unclaimed (`on_text_input` returning
|
||||
/// `None`), which is what lets the caller read it as something else — a
|
||||
/// typed distance, say — instead of guessing beforehand.
|
||||
pub(super) fn feed_command_consumed(&mut self, input: StepInput) -> (Task<Message>, bool) {
|
||||
// A command that takes associative dimensions only keeps just those:
|
||||
// the rest leave the selection and the count of what went is reported,
|
||||
// as the reference does while gathering.
|
||||
let input = self.filter_associative_dimension_selection(input);
|
||||
// Selection keywords (P / PREVIOUS, L / LAST) consume the token
|
||||
// before it reaches the command (#426).
|
||||
if let StepInput::Text(s) = &input {
|
||||
@@ -6179,15 +6230,18 @@ impl OpenCADStudio {
|
||||
CmdResult::Dispatch(cmd) => {
|
||||
// End this interactive front-end, then run the assembled command
|
||||
// through the normal dispatcher. Selection is left untouched.
|
||||
// DIMCONSTRAINT's Radius option becomes its next default; the
|
||||
// DCRADIUS command run on its own leaves the default alone.
|
||||
if cmd == "DCRADIUS"
|
||||
&& self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.name() == "DIMCONSTRAINT")
|
||||
{
|
||||
self.dim_constraint_last = "Radius";
|
||||
// The option picked in DIMCONSTRAINT becomes its next default;
|
||||
// the same command run on its own leaves the default alone.
|
||||
let from_menu = self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.name() == "DIMCONSTRAINT");
|
||||
if from_menu {
|
||||
match cmd.as_str() {
|
||||
"DCRADIUS" => self.dim_constraint_last = "Radius",
|
||||
"DCCONVERT" => self.dim_constraint_last = "Convert",
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
|
||||
+191
-79
@@ -1310,92 +1310,204 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
// A pre-selection is not used: the reference always asks, and it
|
||||
// gathers associative dimensions only.
|
||||
"DCCONVERT" => {
|
||||
self.dim_constraint_last = "Convert";
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
let sel = SelectObjectsCommand::associative_dimensions(
|
||||
cmd,
|
||||
"DCCONVERTAPPLY",
|
||||
"Select associative dimensions to convert:",
|
||||
);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
}
|
||||
|
||||
// The gathered dimensions become dimensional constraints: each one
|
||||
// keeps its dimension, which turns into the constraint's dynamic
|
||||
// (or annotational) dimension driven by a new parameter.
|
||||
"DCCONVERTAPPLY" => {
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
use crate::scene::parametric_constraints::{
|
||||
dynamic_dimension_text, next_angular_parameter_name,
|
||||
next_dimensional_parameter_name, next_radial_parameter_name,
|
||||
ConstraintKind,
|
||||
};
|
||||
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let conversions: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|handle| !self.tabs[i].scene.is_layer_locked(*handle))
|
||||
.filter_map(|handle| {
|
||||
let (kind, refs, value) =
|
||||
crate::scene::dimension_assoc::constraint_from_associative_dimension(
|
||||
&self.tabs[i].scene.document,
|
||||
handle,
|
||||
)?;
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.validate_parametric_constraint(kind, &refs, Some(&value))
|
||||
.ok()?;
|
||||
Some((handle, kind, refs, value))
|
||||
})
|
||||
.collect();
|
||||
if conversions.is_empty() {
|
||||
self.command_line
|
||||
.push_output("No supported associative dimensions were selected.");
|
||||
return None;
|
||||
let annotational = self.constraint_form_annotational;
|
||||
let mut table = self.tabs[i].scene.named_parameters().clone();
|
||||
let mut conversions = Vec::new();
|
||||
let mut refused = 0usize;
|
||||
for handle in handles {
|
||||
if self.tabs[i].scene.is_layer_locked(handle) {
|
||||
refused += 1;
|
||||
continue;
|
||||
}
|
||||
let before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
||||
scope,
|
||||
)
|
||||
});
|
||||
let mut touched: Vec<_> = conversions
|
||||
.iter()
|
||||
.flat_map(|(_, _, refs, _)| refs.iter().map(|reference| reference.entity))
|
||||
.collect();
|
||||
touched.sort_unstable();
|
||||
touched.dedup();
|
||||
let dimensions: Vec<_> = conversions
|
||||
.iter()
|
||||
.map(|(handle, _, _, _)| *handle)
|
||||
.collect();
|
||||
let pending = self.begin_undo(
|
||||
i,
|
||||
"Convert dimensions to constraints",
|
||||
touched.len() + dimensions.len(),
|
||||
false,
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, before);
|
||||
for (_, kind, refs, value) in conversions {
|
||||
let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
|
||||
kind,
|
||||
refs,
|
||||
Some(value),
|
||||
);
|
||||
self.tabs[i].scene.note_parametric_constraint_applied(
|
||||
scope,
|
||||
id,
|
||||
self.constraint_bar_display,
|
||||
);
|
||||
// A dimension already driving a constraint is not converted
|
||||
// a second time.
|
||||
if crate::scene::parametric_constraints::dynamic_dimension_constraint(
|
||||
&self.tabs[i].scene.parametric_constraints,
|
||||
handle,
|
||||
)
|
||||
.is_some()
|
||||
{
|
||||
refused += 1;
|
||||
continue;
|
||||
}
|
||||
self.tabs[i].scene.erase_entities(&dimensions);
|
||||
let changes: Vec<_> = touched
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
let Some((kind, refs, value)) =
|
||||
crate::scene::dimension_assoc::constraint_from_associative_dimension(
|
||||
&self.tabs[i].scene.document,
|
||||
handle,
|
||||
)
|
||||
else {
|
||||
refused += 1;
|
||||
continue;
|
||||
};
|
||||
let DrivingValue::Literal(measured) = value else {
|
||||
refused += 1;
|
||||
continue;
|
||||
};
|
||||
if self.tabs[i]
|
||||
.scene
|
||||
.validate_parametric_constraint(kind, &refs, Some(&DrivingValue::Literal(measured)))
|
||||
.is_err()
|
||||
{
|
||||
refused += 1;
|
||||
continue;
|
||||
}
|
||||
// The parameter is named after the constraint it drives,
|
||||
// as a constraint made by hand would be.
|
||||
let name = match kind {
|
||||
ConstraintKind::Radius => next_radial_parameter_name(&table, false),
|
||||
ConstraintKind::Diameter => next_radial_parameter_name(&table, true),
|
||||
ConstraintKind::Angle | ConstraintKind::Angle3Point => {
|
||||
next_angular_parameter_name(&table)
|
||||
}
|
||||
_ => next_dimensional_parameter_name(&table),
|
||||
};
|
||||
let expression =
|
||||
crate::scene::parametric_constraints::measured_expression(measured);
|
||||
if table.set(&name, &expression).is_err() {
|
||||
refused += 1;
|
||||
continue;
|
||||
}
|
||||
conversions.push((handle, kind, refs, name, expression, measured));
|
||||
}
|
||||
if conversions.is_empty() {
|
||||
self.command_line
|
||||
.push_output("0 associative dimensions converted");
|
||||
self.command_line.push_output(
|
||||
format!("{} dimension(s) converted.", dimensions.len()).as_str(),
|
||||
format!("{refused} associative dimension(s) could not be converted")
|
||||
.as_str(),
|
||||
);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.refresh_properties();
|
||||
return None;
|
||||
}
|
||||
let constraints_before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
|
||||
});
|
||||
let mut touched: Vec<_> = conversions
|
||||
.iter()
|
||||
.flat_map(|(_, _, refs, _, _, _)| refs.iter().map(|r| r.entity))
|
||||
.collect();
|
||||
touched.sort_unstable();
|
||||
touched.dedup();
|
||||
// The constraints layer is made before the recording opens: a
|
||||
// new layer inside it would cost the delta its entity undo.
|
||||
if !annotational {
|
||||
self.tabs[i].scene.ensure_dynamic_dimension_layer();
|
||||
}
|
||||
let pending = self.begin_undo(
|
||||
i,
|
||||
"Convert dimensions to constraints",
|
||||
touched.len() + conversions.len(),
|
||||
false,
|
||||
);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
self.tabs[i].scene.record_undo_named_parameters_before();
|
||||
self.tabs[i].scene.named_parameters = table;
|
||||
let format = self.tabs[i].scene.constraint_name_format;
|
||||
let converted = conversions.len();
|
||||
for (handle, kind, refs, name, expression, measured) in conversions {
|
||||
let angular =
|
||||
matches!(kind, ConstraintKind::Angle | ConstraintKind::Angle3Point);
|
||||
let decimals = angular.then(|| {
|
||||
crate::scene::parametric_constraints::angle_decimals(
|
||||
&self.tabs[i].scene.document,
|
||||
None,
|
||||
)
|
||||
});
|
||||
let text = dynamic_dimension_text(
|
||||
&name,
|
||||
measured,
|
||||
format,
|
||||
false,
|
||||
Some(&expression),
|
||||
annotational,
|
||||
decimals,
|
||||
);
|
||||
if let Some(acadrust::EntityType::Dimension(mut dimension)) =
|
||||
self.tabs[i].scene.document.get_entity(handle).cloned()
|
||||
{
|
||||
let before = self.tabs[i].scene.document.get_entity_arc(handle);
|
||||
self.tabs[i].scene.record_undo_before(handle, before);
|
||||
crate::entities::dimension::set_dimension_text_override(
|
||||
dimension.base_mut(),
|
||||
Some(text),
|
||||
);
|
||||
// The dynamic form lives on the reference's hidden
|
||||
// constraints layer in the constraint grey; the
|
||||
// annotational form stays where the dimension is.
|
||||
if !annotational {
|
||||
dimension.base_mut().common.layer =
|
||||
crate::scene::parametric_constraints::DYNAMIC_DIMENSION_LAYER
|
||||
.to_string();
|
||||
dimension.base_mut().common.color = acadrust::types::Color::Rgb {
|
||||
r: 103,
|
||||
g: 109,
|
||||
b: 118,
|
||||
};
|
||||
}
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.update_entity(acadrust::EntityType::Dimension(dimension));
|
||||
}
|
||||
let set = self.tabs[i].scene.parametric_constraint_set_mut(scope);
|
||||
let id = set.add(kind, refs, Some(DrivingValue::Named(name)));
|
||||
set.dimensions.insert(id, handle);
|
||||
self.tabs[i].scene.note_parametric_constraint_applied(
|
||||
scope,
|
||||
id,
|
||||
self.constraint_bar_display,
|
||||
);
|
||||
}
|
||||
let changes: Vec<_> = touched
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].scene.refresh_hidden_dynamic_dimensions();
|
||||
self.tabs[i].scene.refresh_dynamic_dimension_scales(true);
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
self.command_line
|
||||
.push_output(format!("{converted} associative dimensions converted").as_str());
|
||||
self.command_line.push_output(
|
||||
format!("{refused} associative dimension(s) could not be converted").as_str(),
|
||||
);
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2782,6 +2782,13 @@ pub trait CadCommand: Send {
|
||||
false
|
||||
}
|
||||
|
||||
/// The command takes associative dimensions only. The host drops every
|
||||
/// other object from a completed selection, says how many it dropped, and
|
||||
/// hands the command what is left.
|
||||
fn selection_keeps_associative_dimensions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Called after a selection action completes while `is_selection_gathering` is true.
|
||||
/// `handles` is the full set of currently selected entities.
|
||||
/// Return `Relaunch` to fire the pending command, or `NeedPoint` to keep gathering.
|
||||
|
||||
@@ -36,6 +36,11 @@ pub struct SelectObjectsCommand {
|
||||
pick_crossing: bool,
|
||||
/// Add the command-specific Settings keyword used by Auto Constrain.
|
||||
auto_constrain_settings: bool,
|
||||
/// A command-specific prompt replacing the generic "Select objects:".
|
||||
prompt_text: Option<&'static str>,
|
||||
/// The command works on associative dimensions only; the host drops
|
||||
/// everything else from each completed selection.
|
||||
associative_dimensions_only: bool,
|
||||
}
|
||||
|
||||
impl SelectObjectsCommand {
|
||||
@@ -50,6 +55,8 @@ impl SelectObjectsCommand {
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +72,8 @@ impl SelectObjectsCommand {
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +89,8 @@ impl SelectObjectsCommand {
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,9 +106,24 @@ impl SelectObjectsCommand {
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
prompt_text: None,
|
||||
associative_dimensions_only: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Gather associative dimensions with the command's own wording; the host
|
||||
/// drops every other object from each completed selection.
|
||||
pub fn associative_dimensions(
|
||||
prompt_cmd: &str,
|
||||
pending_cmd: &str,
|
||||
prompt_text: &'static str,
|
||||
) -> Self {
|
||||
let mut command = Self::plain(prompt_cmd, pending_cmd);
|
||||
command.prompt_text = Some(prompt_text);
|
||||
command.associative_dimensions_only = true;
|
||||
command
|
||||
}
|
||||
|
||||
pub fn auto_constrain(pending_cmd: &str) -> Self {
|
||||
let mut command = Self::new(pending_cmd);
|
||||
command.auto_constrain_settings = true;
|
||||
@@ -111,6 +137,11 @@ impl CadCommand for SelectObjectsCommand {
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
// A command with its own wording keeps it whatever is selected, the
|
||||
// way the reference repeats its prompt after every selection.
|
||||
if let (Some(text), None) = (self.prompt_text, self.pick.as_ref()) {
|
||||
return format!("{} {}", self.prompt_cmd, t!(text));
|
||||
}
|
||||
if let Some(pick) = &self.pick {
|
||||
let label = if !pick.closed {
|
||||
t!("Fence")
|
||||
@@ -161,6 +192,10 @@ impl CadCommand for SelectObjectsCommand {
|
||||
self.pick.is_none()
|
||||
}
|
||||
|
||||
fn selection_keeps_associative_dimensions(&self) -> bool {
|
||||
self.associative_dimensions_only
|
||||
}
|
||||
|
||||
// Clickable selection keywords (#426, #596). Window and Crossing fix the
|
||||
// sense of the next box, which dragging would otherwise take from the
|
||||
// direction the corner travels; All takes the whole space; Add and Remove
|
||||
|
||||
Reference in new issue
Block a user