diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 0600de34d..cdeea8ed3 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -966,22 +966,20 @@ impl OpenCADStudio { TableCellEditStart::Started } - /// Removes and re-solves the first conflicting constraint in the active scope. - pub(super) fn resolve_one_parametric_conflict(&mut self) { + fn remove_parametric_constraint( + &mut self, + id: crate::scene::parametric_constraints::ConstraintId, + label: &'static str, + ) -> bool { let i = self.active_tab; let scope = self.tabs[i].current_parametric_scope(); let Some(set) = self.tabs[i].scene.parametric_constraint_set(scope) else { - return; - }; - let Some(&(id, _kind)) = set.conflicts.first() else { - return; + return false; }; let Some(constraint) = set.get(id) else { - return; + return false; }; let touched: Vec = constraint.refs.iter().map(|r| r.entity).collect(); - let label = "Remove conflicting constraint"; - let constraints_before = set.clone(); let pending = self.begin_undo(i, label, touched.len(), true); self.tabs[i] @@ -999,6 +997,30 @@ impl OpenCADStudio { if let Some(pd) = pending { self.commit_undo_delta(i, pd); } + self.refresh_properties(); + true + } + + /// Removes and re-solves the first conflicting constraint in the active scope. + pub(super) fn resolve_one_parametric_conflict(&mut self) { + let i = self.active_tab; + let scope = self.tabs[i].current_parametric_scope(); + let Some(id) = self.tabs[i] + .scene + .parametric_constraint_set(scope) + .and_then(|set| set.conflicts.first().map(|(id, _)| *id)) + else { + return; + }; + self.remove_parametric_constraint(id, "Remove conflicting constraint"); + } + + /// Removes one user-selected constraint from the active standard graph scope. + pub(super) fn delete_parametric_constraint( + &mut self, + id: crate::scene::parametric_constraints::ConstraintId, + ) { + self.remove_parametric_constraint(id, "Delete constraint"); } /// Rebuilds the active tab's `Scene::named_parameters` from the @@ -7867,6 +7889,44 @@ mod parametric_constraint_undo_tests { ); } + #[test] + fn deleting_one_parametric_constraint_is_undoable() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + let line = add_line(&mut app, 0.0, 0.0, 10.0, 3.0); + let _ = app.apply_cmd_result(CmdResult::AddParametricConstraint { + kind: ConstraintKind::Horizontal, + refs: vec![ParametricRef::whole(line)], + driving_param: None, + label: "Horizontal constraint", + }); + let id = app.tabs[app.active_tab] + .scene + .parametric_constraint_set(ParametricScope::ModelSpace) + .unwrap() + .constraints[0] + .id; + + app.delete_parametric_constraint(id); + assert!(app.tabs[app.active_tab] + .scene + .parametric_constraint_set(ParametricScope::ModelSpace) + .unwrap() + .constraints + .is_empty()); + + app.undo_steps(1); + assert_eq!( + app.tabs[app.active_tab] + .scene + .parametric_constraint_set(ParametricScope::ModelSpace) + .unwrap() + .constraints + .len(), + 1 + ); + } + /// `named_parameters_design.md` stage 4: applying the PARAMETERS editor's /// working buffer both defines the parameter and re-solves whatever /// constraint already references it by name — the entire point of a @@ -7950,6 +8010,31 @@ mod parametric_constraint_undo_tests { ); } + /// The no-selection page must expose the active drawing's named parameters. + #[test] + fn no_selection_properties_exposes_named_parameters() { + let mut app = OpenCADStudio::new_for_test(); + let _ = app.automation_op(r#"{"op":"new"}"#); + app.tabs[app.active_tab] + .scene + .named_parameters_mut() + .set("width", "12") + .unwrap(); + + app.refresh_properties(); + + assert!(app.tabs[app.active_tab] + .properties + .sections + .iter() + .flat_map(|section| §ion.props) + .any(|property| matches!( + &property.value, + crate::scene::model::object::PropValue::ParamRow { name, .. } + if name == "width" + ))); + } + /// End-to-end through the actual `Message` handlers a Properties-panel /// click/keystroke would fire — not `apply_named_parameter_editor_rows` /// directly — covering the whole Parameters-section lifecycle: add, diff --git a/src/app/mod.rs b/src/app/mod.rs index 50a864cf0..7f420ab42 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2902,6 +2902,8 @@ pub enum Message { /// A Constraints-section row was clicked: select every entity in the /// list (replacing the current selection) in the viewport. PropConstraintLinkClick(Vec), + /// Remove one parametric constraint from the active standard graph scope. + PropConstraintDelete(crate::scene::parametric_constraints::ConstraintId), // ── About window ──────────────────────────────────────────────────── AboutOpen, // ── Graphics warning ──────────────────────────────────────────────── diff --git a/src/app/properties.rs b/src/app/properties.rs index 393467b2d..abbcebf7c 100644 --- a/src/app/properties.rs +++ b/src/app/properties.rs @@ -484,6 +484,35 @@ impl OpenCADStudio { }, ], }, + PropSection { + title: t!("Parameters").into_owned(), + props: { + let mut props: Vec = scene + .named_parameters() + .iter() + .enumerate() + .map(|(index, parameter)| Property { + label: String::new(), + field: "named_parameter", + value: PropValue::ParamRow { + index, + name: parameter.name.clone(), + formula: parameter.source.clone(), + resolved: scene + .named_parameters() + .resolve(¶meter.name) + .map_err(|error| error.to_string()), + }, + }) + .collect(); + props.push(Property { + label: String::new(), + field: "named_parameter_add", + value: PropValue::ParamAddRow, + }); + props + }, + }, ]; ui::PropertiesPanel { title: t!("No selection").into_owned(), @@ -2202,6 +2231,11 @@ impl OpenCADStudio { let scope = self.tabs[i].current_parametric_scope(); let scene = &self.tabs[i].scene; if let Some(set) = scene.parametric_constraint_set(scope) { + let parameters = if set.local_parameters.is_empty() { + scene.named_parameters() + } else { + &set.local_parameters + }; let props: Vec = set .constraints_touching(handle) .map(|c| { @@ -2219,7 +2253,7 @@ impl OpenCADStudio { crate::scene::named_parameters::DrivingValue::Named( name, ), - ) => match scene.named_parameters().resolve(name) { + ) => match parameters.resolve(name) { Ok(v) => format!(" = {name} ({v:.2})"), Err(_) => format!(" = {name} (?)"), }, @@ -2239,6 +2273,7 @@ impl OpenCADStudio { label, field: "parametric_constraint", value: crate::scene::model::object::PropValue::EntityLink { + id: c.id, handles, conflicting, }, diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 4d3919596..965ac0cce 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -6250,6 +6250,10 @@ impl OpenCADStudio { Message::PropParamCommit { index, field } => self.on_prop_param_commit(index, field), Message::PropParamDelete(index) => self.on_prop_param_delete(index), Message::PropParamAddNew => self.on_prop_param_add_new(), + Message::PropConstraintDelete(id) => { + self.delete_parametric_constraint(id); + Task::none() + } Message::PropConstraintLinkClick(handles) => { let i = self.active_tab; self.tabs[i].scene.deselect_all(); diff --git a/src/modules/parametric/value.rs b/src/modules/parametric/value.rs index de84df66a..94e304f12 100644 --- a/src/modules/parametric/value.rs +++ b/src/modules/parametric/value.rs @@ -331,6 +331,12 @@ impl CadCommand for DistanceConstraintCommand { } fn on_text_input(&mut self, text: &str) -> Option { + // An existing parameter name wins over a mode keyword with the same spelling. + if let Some(value @ DrivingValue::Named(_)) = + parse_driving_value(text, &self.known_param_names) + { + return self.build(value); + } // Mode-switch keywords re-prompt instead of building — consumed // inputs return `Some(NeedPoint)` so the driver doesn't re-offer // the same token a second time (matches e.g. `trim.rs`'s @@ -605,6 +611,24 @@ mod tests { )); } + #[test] + fn a_parameter_named_like_a_mode_keyword_remains_usable() { + let mut scene = Scene::new(); + let circle = add_circle(&mut scene); + scene.named_parameters_mut().set("r", "5").unwrap(); + let mut cmd = + DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT"); + + assert!(matches!( + cmd.on_text_input("R"), + Some(CmdResult::AddParametricConstraint { + kind: ConstraintKind::Radius, + driving_param: Some(DrivingValue::Named(name)), + .. + }) if name == "r" + )); + } + #[test] fn ordinary_distances_reject_nonpositive_and_nonfinite_values() { let mut scene = Scene::new(); diff --git a/src/scene/model/object.rs b/src/scene/model/object.rs index 85e40c6d7..e2b228d3d 100644 --- a/src/scene/model/object.rs +++ b/src/scene/model/object.rs @@ -62,8 +62,10 @@ pub enum PropValue { /// row for one parametric constraint. Clicking it selects every /// entity in `handles` in the viewport. `conflicting` mirrors /// `ParametricConstraintSet::conflicts`, tinting the row the same danger - /// color the constraint's glyph pill already uses. + /// color the constraint's glyph pill already uses. `id` routes the row's + /// delete action back to that constraint in the active scope. EntityLink { + id: crate::scene::parametric_constraints::ConstraintId, handles: Vec, conflicting: bool, }, diff --git a/src/ui/properties.rs b/src/ui/properties.rs index 507c150d6..59a11dedb 100644 --- a/src/ui/properties.rs +++ b/src/ui/properties.rs @@ -865,8 +865,8 @@ impl PropertiesPanel { self.render_hatch_pattern_row(label, current) } PropValue::AttrText { tag, value } => self.render_attr_row(tag, value), - PropValue::EntityLink { handles, conflicting } => { - render_entity_link_row(label, handles.clone(), *conflicting) + PropValue::EntityLink { id, handles, conflicting } => { + render_entity_link_row(label, *id, handles.clone(), *conflicting) } PropValue::ParamRow { index, name, formula, resolved } => { self.render_param_row(*index, name, formula, resolved) @@ -1933,8 +1933,13 @@ fn render_ro_with_tooltip_row<'a>( /// field, so this doesn't use the usual `prop_row_widget` label|value split. /// Clicking it selects every entity in `handles`; a conflicting/redundant /// constraint (mirrors the viewport glyph pill's own color cue) tints red. -fn render_entity_link_row<'a>(label: &'a str, handles: Vec, conflicting: bool) -> Element<'a, Message> { - let btn = button(text(label).size(FONT_SZ).width(Length::Fill)) +fn render_entity_link_row<'a>( + label: &'a str, + id: crate::scene::parametric_constraints::ConstraintId, + handles: Vec, + conflicting: bool, +) -> Element<'a, Message> { + let link = button(text(label).size(FONT_SZ).width(Length::Fill)) .on_press(Message::PropConstraintLinkClick(handles)) .style(move |theme: &Theme, status| { let palette = theme.palette(); @@ -1955,7 +1960,13 @@ fn render_entity_link_row<'a>(label: &'a str, handles: Vec, conflicting: }) .padding([3, 8]) .width(Length::Fill); - container(btn).width(Length::Fill).into() + let delete = button(text("\u{2715}").size(FONT_SZ)) + .on_press(Message::PropConstraintDelete(id)) + .style(button::text) + .padding([2, 6]); + container(row![link, delete].spacing(2).align_y(iced::Center)) + .width(Length::Fill) + .into() } // ── Parameters section: "+ Add parameter" row ──────────────────────────────