diff --git a/Cargo.lock b/Cargo.lock index 6563ff46e..53b52ebd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -896,7 +896,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "cadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6ceda43233e59224b7683a69917b62ab26ea3ec3#6ceda43233e59224b7683a69917b62ab26ea3ec3" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a2e7cee23d5b1fd6e1eef16118fa6db491febe8b#a2e7cee23d5b1fd6e1eef16118fa6db491febe8b" dependencies = [ "acadrust", "cavalier_contours", @@ -907,7 +907,7 @@ dependencies = [ [[package]] name = "cadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6ceda43233e59224b7683a69917b62ab26ea3ec3#6ceda43233e59224b7683a69917b62ab26ea3ec3" +source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=a2e7cee23d5b1fd6e1eef16118fa6db491febe8b#a2e7cee23d5b1fd6e1eef16118fa6db491febe8b" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 86f8b894a..2ca524939 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,8 +32,8 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] } -cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6ceda43233e59224b7683a69917b62ab26ea3ec3", features = ["acis", "offset"] } -cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6ceda43233e59224b7683a69917b62ab26ea3ec3" } +cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a2e7cee23d5b1fd6e1eef16118fa6db491febe8b", features = ["acis", "offset"] } +cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "a2e7cee23d5b1fd6e1eef16118fa6db491febe8b" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } flate2 = "1" image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] } diff --git a/assets/icons/constrain/auto.svg b/assets/icons/constrain/auto.svg new file mode 100644 index 000000000..32a709f5f --- /dev/null +++ b/assets/icons/constrain/auto.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/constrain/convert.svg b/assets/icons/constrain/convert.svg new file mode 100644 index 000000000..b4a7baec6 --- /dev/null +++ b/assets/icons/constrain/convert.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/delete.svg b/assets/icons/constrain/delete.svg new file mode 100644 index 000000000..8c8d5234f --- /dev/null +++ b/assets/icons/constrain/delete.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/distance_x.svg b/assets/icons/constrain/distance_x.svg new file mode 100644 index 000000000..1f30c8e00 --- /dev/null +++ b/assets/icons/constrain/distance_x.svg @@ -0,0 +1,5 @@ + + + + X + diff --git a/assets/icons/constrain/distance_y.svg b/assets/icons/constrain/distance_y.svg new file mode 100644 index 000000000..7c6a0c26c --- /dev/null +++ b/assets/icons/constrain/distance_y.svg @@ -0,0 +1,5 @@ + + + + Y + diff --git a/assets/icons/constrain/hide_all.svg b/assets/icons/constrain/hide_all.svg new file mode 100644 index 000000000..99f0ad1d0 --- /dev/null +++ b/assets/icons/constrain/hide_all.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/parameters.svg b/assets/icons/constrain/parameters.svg new file mode 100644 index 000000000..10fd00670 --- /dev/null +++ b/assets/icons/constrain/parameters.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/show.svg b/assets/icons/constrain/show.svg new file mode 100644 index 000000000..9338b3172 --- /dev/null +++ b/assets/icons/constrain/show.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/constrain/show_all.svg b/assets/icons/constrain/show_all.svg new file mode 100644 index 000000000..dccd715da --- /dev/null +++ b/assets/icons/constrain/show_all.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/constrain/smooth.svg b/assets/icons/constrain/smooth.svg new file mode 100644 index 000000000..5986ce9d0 --- /dev/null +++ b/assets/icons/constrain/smooth.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/dim_aligned.svg b/assets/icons/dim_aligned.svg index b5489882e..bcf4f458d 100644 --- a/assets/icons/dim_aligned.svg +++ b/assets/icons/dim_aligned.svg @@ -1,7 +1,7 @@ - - - - - + + + + + diff --git a/crates/ocs_plugin_api/src/ribbon.rs b/crates/ocs_plugin_api/src/ribbon.rs index 60e5784a5..28ba7b116 100644 --- a/crates/ocs_plugin_api/src/ribbon.rs +++ b/crates/ocs_plugin_api/src/ribbon.rs @@ -74,6 +74,8 @@ pub struct ToolDef { pub enum RibbonItem { /// 1-row button — icon only, no label. Tool(ToolDef), + /// 1-row button — icon and label. + LabeledTool(ToolDef), /// 3-row button — icon + label below; full ribbon height. LargeTool(ToolDef), /// 1-row dropdown — icon + ▾ on right, no label. @@ -83,6 +85,14 @@ pub enum RibbonItem { items: Vec<(&'static str, &'static str, IconKind)>, default: &'static str, }, + /// 1-row split dropdown — icon and fixed label, plus a menu arrow. + LabeledDropdown { + id: &'static str, + label: &'static str, + icon: IconKind, + items: Vec<(&'static str, &'static str, IconKind)>, + default: &'static str, + }, /// 3-row dropdown — icon + label + ▾ below label; full ribbon height. LargeDropdown { id: &'static str, @@ -91,6 +101,8 @@ pub enum RibbonItem { items: Vec<(&'static str, &'static str, IconKind)>, default: &'static str, }, + /// Explicit columns of small icon-only buttons. + ToolGrid { columns: Vec> }, /// Layer combo + two rows of small tools below. /// row2: operates on the layer of a selected object (off/freeze/lock/make-current) /// row3: all-layers operations + match (on/thaw/unlock/match) diff --git a/crates/ocs_plugin_api/src/ribbon/owned.rs b/crates/ocs_plugin_api/src/ribbon/owned.rs index 4e6f46cf4..239d94026 100644 --- a/crates/ocs_plugin_api/src/ribbon/owned.rs +++ b/crates/ocs_plugin_api/src/ribbon/owned.rs @@ -23,6 +23,7 @@ pub enum OwnedIconKind { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum OwnedRibbonItem { Tool(OwnedToolDef), + LabeledTool(OwnedToolDef), LargeTool(OwnedToolDef), Dropdown { id: String, @@ -30,6 +31,13 @@ pub enum OwnedRibbonItem { items: Vec<(String, String, OwnedIconKind)>, default: String, }, + LabeledDropdown { + id: String, + label: String, + icon: OwnedIconKind, + items: Vec<(String, String, OwnedIconKind)>, + default: String, + }, LargeDropdown { id: String, label: String, @@ -37,6 +45,9 @@ pub enum OwnedRibbonItem { items: Vec<(String, String, OwnedIconKind)>, default: String, }, + ToolGrid { + columns: Vec>, + }, LayerComboGroup { row2: Vec, row3: Vec, @@ -79,11 +90,20 @@ fn push_tool_command(tool: &OwnedToolDef, out: &mut Vec) { fn collect_item_command_ids(item: &OwnedRibbonItem, out: &mut Vec) { match item { - OwnedRibbonItem::Tool(t) | OwnedRibbonItem::LargeTool(t) => push_tool_command(t, out), - OwnedRibbonItem::Dropdown { items, .. } | OwnedRibbonItem::LargeDropdown { items, .. } => { + OwnedRibbonItem::Tool(t) + | OwnedRibbonItem::LabeledTool(t) + | OwnedRibbonItem::LargeTool(t) => push_tool_command(t, out), + OwnedRibbonItem::Dropdown { items, .. } + | OwnedRibbonItem::LabeledDropdown { items, .. } + | OwnedRibbonItem::LargeDropdown { items, .. } => { // Each entry is (command id, label, icon). out.extend(items.iter().map(|(id, _, _)| id.clone())); } + OwnedRibbonItem::ToolGrid { columns } => { + for tool in columns.iter().flatten() { + push_tool_command(tool, out); + } + } OwnedRibbonItem::LayerComboGroup { row2, row3 } => { for t in row2.iter().chain(row3.iter()) { push_tool_command(t, out); @@ -178,6 +198,7 @@ impl From for OwnedRibbonItem { fn from(item: RibbonItem) -> Self { match item { RibbonItem::Tool(t) => OwnedRibbonItem::Tool(t.into()), + RibbonItem::LabeledTool(t) => OwnedRibbonItem::LabeledTool(t.into()), RibbonItem::LargeTool(t) => OwnedRibbonItem::LargeTool(t.into()), RibbonItem::Dropdown { id, @@ -193,6 +214,17 @@ impl From for OwnedRibbonItem { .collect(), default: default.to_string(), }, + RibbonItem::LabeledDropdown { id, label, icon, items, default } => { + OwnedRibbonItem::LabeledDropdown { + id: id.to_string(), + label: label.to_string(), + icon: icon.into(), + items: items.into_iter().map(|(a, b, i)| { + (a.to_string(), b.to_string(), i.into()) + }).collect(), + default: default.to_string(), + } + } RibbonItem::LargeDropdown { id, label, @@ -209,6 +241,11 @@ impl From for OwnedRibbonItem { .collect(), default: default.to_string(), }, + RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid { + columns: columns.into_iter().map(|column| { + column.into_iter().map(Into::into).collect() + }).collect(), + }, RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { row2: row2.into_iter().map(Into::into).collect(), row3: row3.into_iter().map(Into::into).collect(), @@ -238,6 +275,7 @@ impl OwnedRibbonItem { pub fn to_static(self) -> RibbonItem { match self { OwnedRibbonItem::Tool(t) => RibbonItem::Tool(t.to_static()), + OwnedRibbonItem::LabeledTool(t) => RibbonItem::LabeledTool(t.to_static()), OwnedRibbonItem::LargeTool(t) => RibbonItem::LargeTool(t.to_static()), OwnedRibbonItem::Dropdown { id, @@ -259,6 +297,19 @@ impl OwnedRibbonItem { .collect(), default: &*Box::leak(default.into_boxed_str()), }, + OwnedRibbonItem::LabeledDropdown { id, label, icon, items, default } => { + RibbonItem::LabeledDropdown { + id: &*Box::leak(id.into_boxed_str()), + label: &*Box::leak(label.into_boxed_str()), + icon: icon.to_static(), + items: items.into_iter().map(|(a, b, i)| ( + &*Box::leak(a.into_boxed_str()), + &*Box::leak(b.into_boxed_str()), + i.to_static(), + )).collect(), + default: &*Box::leak(default.into_boxed_str()), + } + } OwnedRibbonItem::LargeDropdown { id, label, @@ -281,6 +332,11 @@ impl OwnedRibbonItem { .collect(), default: &*Box::leak(default.into_boxed_str()), }, + OwnedRibbonItem::ToolGrid { columns } => RibbonItem::ToolGrid { + columns: columns.into_iter().map(|column| { + column.into_iter().map(OwnedToolDef::to_static).collect() + }).collect(), + }, OwnedRibbonItem::LayerComboGroup { row2, row3 } => RibbonItem::LayerComboGroup { row2: row2.into_iter().map(|t| t.to_static()).collect(), row3: row3.into_iter().map(|t| t.to_static()).collect(), @@ -310,6 +366,7 @@ impl From<&RibbonItem> for OwnedRibbonItem { fn from(item: &RibbonItem) -> Self { match item { RibbonItem::Tool(t) => OwnedRibbonItem::Tool(t.into()), + RibbonItem::LabeledTool(t) => OwnedRibbonItem::LabeledTool(t.into()), RibbonItem::LargeTool(t) => OwnedRibbonItem::LargeTool(t.into()), RibbonItem::Dropdown { id, @@ -325,6 +382,17 @@ impl From<&RibbonItem> for OwnedRibbonItem { .collect(), default: default.to_string(), }, + RibbonItem::LabeledDropdown { id, label, icon, items, default } => { + OwnedRibbonItem::LabeledDropdown { + id: id.to_string(), + label: label.to_string(), + icon: icon.into(), + items: items.iter().map(|(a, b, i)| { + (a.to_string(), b.to_string(), i.into()) + }).collect(), + default: default.to_string(), + } + } RibbonItem::LargeDropdown { id, label, @@ -341,6 +409,11 @@ impl From<&RibbonItem> for OwnedRibbonItem { .collect(), default: default.to_string(), }, + RibbonItem::ToolGrid { columns } => OwnedRibbonItem::ToolGrid { + columns: columns.iter().map(|column| { + column.iter().map(Into::into).collect() + }).collect(), + }, RibbonItem::LayerComboGroup { row2, row3 } => OwnedRibbonItem::LayerComboGroup { row2: row2.iter().map(Into::into).collect(), row3: row3.iter().map(Into::into).collect(), @@ -518,4 +591,39 @@ mod tests { assert!(ids.contains(&"LS_LABEL".to_string()), "got {ids:?}"); assert!(ids.contains(&"LS_AUTOLABEL".to_string()), "got {ids:?}"); } + + #[test] + fn command_ids_covers_labeled_tools_and_tool_grids() { + let tool = |id: &str| OwnedToolDef { + id: id.to_lowercase(), + label: id.to_string(), + icon: OwnedIconKind::Glyph("·".to_string()), + event: ModuleEvent::Command(id.to_string()), + }; + let group = OwnedRibbonGroup { + title: "Constraints".to_string(), + tools: vec![ + OwnedRibbonItem::LabeledTool(tool("SHOW_ALL")), + OwnedRibbonItem::LabeledDropdown { + id: "visibility".to_string(), + label: "Show/Hide".to_string(), + icon: OwnedIconKind::Glyph("·".to_string()), + items: vec![( + "SHOW_SELECTED".to_string(), + "Show".to_string(), + OwnedIconKind::Glyph("·".to_string()), + )], + default: "SHOW_SELECTED".to_string(), + }, + OwnedRibbonItem::ToolGrid { + columns: vec![vec![tool("PARALLEL"), tool("PERPENDICULAR")]], + }, + ], + }; + + let ids = group.command_ids(); + assert_eq!(ids, ["SHOW_ALL", "SHOW_SELECTED", "PARALLEL", "PERPENDICULAR"]); + let round_trip: OwnedRibbonGroup = group.to_static().into(); + assert_eq!(round_trip.command_ids(), ids); + } } diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 54d31fed1..ae2f9dcbb 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -1871,6 +1871,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: تم إنشاء __ocs_arg_created__ عنصر؛ تعذر تدوير __ocs_arg_failed__ مصدر. .revolve-no-selected-object-could-be-revolved = REVOLVE: تعذر تدوير أي عنصر محدد بهذه الخيارات. + .auto-constrain = قيد تلقائي + .show-all = إظهار الكل + .hide-all = إخفاء الكل + .delete-constraints = حذف القيود + .parameters-manager = مدير المعلمات + .geometric = هندسي + .dimensional = بُعدي + .show-hide = إظهار/إخفاء + errors = .save-warning = تحذير الحفظ .unable-to-save-drawing = تعذر حفظ الرسم diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 44cff1f04..44b46ade8 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: създадени обекти: __ocs_arg_created__; неуспешно завъртени източници: __ocs_arg_failed__. .revolve-no-selected-object-could-be-revolved = REVOLVE: нито един избран обект не може да се завърти с тези опции. + .auto-constrain = Автоматично ограничаване + .show-all = Показване на всички + .hide-all = Скриване на всички + .delete-constraints = Изтриване на ограничения + .parameters-manager = Диспечер на параметри + .geometric = Геометрични + .dimensional = Размерни + .show-hide = Показване/скриване + errors = .save-warning = Предупреждение при запазване .unable-to-save-drawing = Чертежът не може да бъде запазен diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index b39165ee4..ff24f0b1f 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: vytvořené objekty: __ocs_arg_created__; zdroje s neúspěšnou rotací: __ocs_arg_failed__. .revolve-no-selected-object-could-be-revolved = REVOLVE: žádný vybraný objekt nelze rotovat s těmito možnostmi. + .auto-constrain = Automatické vazby + .show-all = Zobrazit vše + .hide-all = Skrýt vše + .delete-constraints = Odstranit vazby + .parameters-manager = Správce parametrů + .geometric = Geometrické + .dimensional = Rozměrové + .show-hide = Zobrazit/skrýt + errors = .save-warning = Uložit varování .unable-to-save-drawing = Nákres nelze uložit diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index b071c1696..bad405a19 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -1859,6 +1859,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ Objekt(e) erstellt; __ocs_arg_failed__ Quelle(n) konnten nicht rotiert werden. .revolve-no-selected-object-could-be-revolved = REVOLVE: Kein ausgewähltes Objekt konnte mit diesen Optionen rotiert werden. + .auto-constrain = Automatisch beschränken + .show-all = Alle anzeigen + .hide-all = Alle ausblenden + .delete-constraints = Abhängigkeiten löschen + .parameters-manager = Parameter-Manager + .geometric = Geometrisch + .dimensional = Bemaßung + .show-hide = Anzeigen/Ausblenden + errors = .save-warning = Warnhinweise speichern .unable-to-save-drawing = Zeichnung konnte nicht gespeichert werden diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 4a2bde2f7..652a44b80 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .dwgunits-convert-drawing-to = DWGUNITS { common.convert }: { common.drawing } → .opened-plot-preview = { plot.preview } ✓ + .auto-constrain = Αυτόματοι περιορισμοί + .show-all = Εμφάνιση όλων + .hide-all = Απόκρυψη όλων + .delete-constraints = Διαγραφή περιορισμών + .parameters-manager = Διαχείριση παραμέτρων + .geometric = Γεωμετρικοί + .dimensional = Διαστατικοί + .show-hide = Εμφάνιση/Απόκρυψη + errors = .save-warning = Προειδοποίηση αποθήκευσης .unable-to-save-drawing = Αδυναμία αποθήκευσης σχεδίου diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index f911e3b11..a0d1f0b71 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -1859,6 +1859,15 @@ common = .dwgunits-convert-drawing-to = DWGUNITS Convert drawing to: .opened-plot-preview = Opened plot preview. + .auto-constrain = Auto Constrain + .show-all = Show All + .hide-all = Hide All + .delete-constraints = Delete Constraints + .parameters-manager = Parameters Manager + .geometric = Geometric + .dimensional = Dimensional + .show-hide = Show/Hide + errors = .save-warning = Save Warning .unable-to-save-drawing = Unable to Save Drawing diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 6bc0e3121..72ddf64f4 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -1859,6 +1859,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ objetos creados; no se pudieron revolucionar __ocs_arg_failed__ fuentes. .revolve-no-selected-object-could-be-revolved = REVOLVE: no se pudo revolucionar ningún objeto seleccionado con estas opciones. + .auto-constrain = Restricción automática + .show-all = Mostrar todo + .hide-all = Ocultar todo + .delete-constraints = Eliminar restricciones + .parameters-manager = Administrador de parámetros + .geometric = Geométricas + .dimensional = Dimensionales + .show-hide = Mostrar/Ocultar + errors = .save-warning = Advertencia de guardado .unable-to-save-drawing = No se puede guardar el dibujo diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 8e34ba5b3..f8ece8573 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: luotu __ocs_arg_created__ objektia; __ocs_arg_failed__ lähteen pyöräytys epäonnistui. .revolve-no-selected-object-could-be-revolved = REVOLVE: yhtäkään valittua objektia ei voitu pyöräyttää näillä asetuksilla. + .auto-constrain = Automaattiset rajoitteet + .show-all = Näytä kaikki + .hide-all = Piilota kaikki + .delete-constraints = Poista rajoitteet + .parameters-manager = Parametrien hallinta + .geometric = Geometriset + .dimensional = Mitalliset + .show-hide = Näytä/Piilota + errors = .save-warning = Tallenna varoitus .unable-to-save-drawing = Piirustusta ei voi tallentaa diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index c35959341..56c3ec309 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -1859,6 +1859,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE : __ocs_arg_created__ objet(s) créé(s) ; révolution impossible pour __ocs_arg_failed__ source(s). .revolve-no-selected-object-could-be-revolved = REVOLVE : aucun objet sélectionné n’a pu être révolutionné avec ces options. + .auto-constrain = Contraintes automatiques + .show-all = Tout afficher + .hide-all = Tout masquer + .delete-constraints = Supprimer les contraintes + .parameters-manager = Gestionnaire de paramètres + .geometric = Géométriques + .dimensional = Dimensionnelles + .show-hide = Afficher/Masquer + errors = .save-warning = Enregistrer l' avertissement .unable-to-save-drawing = Impossible de sauvegarder le dessin diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index cc87a6003..a237913db 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -1847,6 +1847,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ ऑब्जेक्ट बने; __ocs_arg_failed__ स्रोत घुमाए नहीं जा सके। .revolve-no-selected-object-could-be-revolved = REVOLVE: इन विकल्पों से कोई चयनित ऑब्जेक्ट घुमाया नहीं जा सका। + .auto-constrain = स्वचालित बाधा + .show-all = सभी दिखाएँ + .hide-all = सभी छिपाएँ + .delete-constraints = बाधाएँ हटाएँ + .parameters-manager = पैरामीटर प्रबंधक + .geometric = ज्यामितीय + .dimensional = आयामी + .show-hide = दिखाएँ/छिपाएँ + errors = .save-warning = चेतावनी सहेजें .unable-to-save-drawing = ड्रॉइंग सहेजी नहीं जा सकी diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index faf864c3d..83e0202fa 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: létrehozott objektumok: __ocs_arg_created__; sikertelen megforgatások: __ocs_arg_failed__. .revolve-no-selected-object-could-be-revolved = REVOLVE: egyik kijelölt objektum sem forgatható meg ezekkel a beállításokkal. + .auto-constrain = Automatikus kényszerezés + .show-all = Összes megjelenítése + .hide-all = Összes elrejtése + .delete-constraints = Kényszerek törlése + .parameters-manager = Paraméterkezelő + .geometric = Geometriai + .dimensional = Méretkényszerek + .show-hide = Megjelenítés/Elrejtés + errors = .save-warning = Figyelmeztetés mentése .unable-to-save-drawing = Nem sikerült menteni a rajzot diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index ebc23c1ad..2e1a28c2c 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ oggetti creati; impossibile rivoluzionare __ocs_arg_failed__ sorgenti. .revolve-no-selected-object-could-be-revolved = REVOLVE: nessun oggetto selezionato può essere rivoluzionato con queste opzioni. + .auto-constrain = Vincolo automatico + .show-all = Mostra tutto + .hide-all = Nascondi tutto + .delete-constraints = Elimina vincoli + .parameters-manager = Gestione parametri + .geometric = Geometrici + .dimensional = Dimensionali + .show-hide = Mostra/Nascondi + errors = .save-warning = Salva avviso .unable-to-save-drawing = Impossibile salvare il disegno diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 660821c97..e14ae20c2 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -1849,6 +1849,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ 個のオブジェクトを作成しました。__ocs_arg_failed__ 個の元の図形は回転できませんでした。 .revolve-no-selected-object-could-be-revolved = REVOLVE: 指定したオプションでは、選択したどのオブジェクトも回転できませんでした。 + .auto-constrain = 自動拘束 + .show-all = すべて表示 + .hide-all = すべて非表示 + .delete-constraints = 拘束を削除 + .parameters-manager = パラメータ管理 + .geometric = 幾何拘束 + .dimensional = 寸法拘束 + .show-hide = 表示/非表示 + errors = .save-warning = 保存の警告 .unable-to-save-drawing = 図面を保存できません diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index befedde8e..05151a434 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: 객체 __ocs_arg_created__개 생성, 원본 __ocs_arg_failed__개 회전 실패. .revolve-no-selected-object-could-be-revolved = REVOLVE: 지정한 옵션으로 선택한 객체를 회전할 수 없습니다. + .auto-constrain = 자동 구속 + .show-all = 모두 표시 + .hide-all = 모두 숨기기 + .delete-constraints = 구속 삭제 + .parameters-manager = 매개변수 관리자 + .geometric = 기하 구속 + .dimensional = 치수 구속 + .show-hide = 표시/숨기기 + errors = .save-warning = 경고 저장 .unable-to-save-drawing = 도면을 저장할 수 없습니다 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 6a6729ec9..600feb8a4 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -1859,6 +1859,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ object(en) gemaakt; __ocs_arg_failed__ bron(nen) konden niet worden gewenteld. .revolve-no-selected-object-could-be-revolved = REVOLVE: geen geselecteerd object kon met deze opties worden gewenteld. + .auto-constrain = Automatisch beperken + .show-all = Alles tonen + .hide-all = Alles verbergen + .delete-constraints = Beperkingen verwijderen + .parameters-manager = Parameterbeheer + .geometric = Geometrisch + .dimensional = Maatvoering + .show-hide = Tonen/Verbergen + errors = .save-warning = Waarschuwing opslaan .unable-to-save-drawing = Kan tekening niet opslaan diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 7edaa4abf..b8d7b9e80 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: utworzono __ocs_arg_created__ obiektów; obrót __ocs_arg_failed__ źródeł nie powiódł się. .revolve-no-selected-object-could-be-revolved = REVOLVE: nie udało się obrócić żadnego zaznaczonego obiektu z tymi opcjami. + .auto-constrain = Więzy automatyczne + .show-all = Pokaż wszystko + .hide-all = Ukryj wszystko + .delete-constraints = Usuń więzy + .parameters-manager = Menedżer parametrów + .geometric = Geometryczne + .dimensional = Wymiarowe + .show-hide = Pokaż/Ukryj + errors = .save-warning = Zapisz ostrzeżenie .unable-to-save-drawing = Nie można zapisać rysunku diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 68e95bdb4..f5a0507f4 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -1859,6 +1859,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: __ocs_arg_created__ objeto(s) criado(s); revolução impossível para __ocs_arg_failed__ origem(ns). .revolve-no-selected-object-could-be-revolved = REVOLVE: nenhum objeto selecionado pôde ser revolucionado com estas opções. + .auto-constrain = Restringir automaticamente + .show-all = Mostrar tudo + .hide-all = Ocultar tudo + .delete-constraints = Excluir restrições + .parameters-manager = Gerenciador de parâmetros + .geometric = Geométricas + .dimensional = Dimensionais + .show-hide = Mostrar/Ocultar + errors = .save-warning = Aviso de salvamento .unable-to-save-drawing = Não foi possível salvar o desenho diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 4fbb61202..6dfa348e3 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -1862,6 +1862,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE: создано объектов: __ocs_arg_created__; не удалось повернуть источников: __ocs_arg_failed__. .revolve-no-selected-object-could-be-revolved = REVOLVE: ни один выбранный объект не удалось повернуть с этими параметрами. + .auto-constrain = Автоматические зависимости + .show-all = Показать все + .hide-all = Скрыть все + .delete-constraints = Удалить зависимости + .parameters-manager = Диспетчер параметров + .geometric = Геометрические + .dimensional = Размерные + .show-hide = Показать/Скрыть + errors = .save-warning = Спасти предостережение .unable-to-save-drawing = Не удалось сохранить чертёж diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 06efb10d2..f0538ada9 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -1843,6 +1843,15 @@ common = .dwgunits-convert-drawing-to = DWGUNITS { common.convert }: { common.drawing } → .opened-plot-preview = { plot.preview } ✓ + .auto-constrain = Otomatik Kısıtla + .show-all = Tümünü Göster + .hide-all = Tümünü Gizle + .delete-constraints = Kısıtlamaları Sil + .parameters-manager = Parametre Yöneticisi + .geometric = Geometrik + .dimensional = Boyutsal + .show-hide = Göster/Gizle + errors = .save-warning = Kaydetme Uyarısı .unable-to-save-drawing = Çizim Kaydedilemiyor diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index e6d679313..ba629a629 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -1846,6 +1846,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE:已创建 __ocs_arg_created__ 个对象;__ocs_arg_failed__ 个源对象无法旋转。 .revolve-no-selected-object-could-be-revolved = REVOLVE:无法使用指定选项旋转任何选定对象。 + .auto-constrain = 自动约束 + .show-all = 全部显示 + .hide-all = 全部隐藏 + .delete-constraints = 删除约束 + .parameters-manager = 参数管理器 + .geometric = 几何 + .dimensional = 标注 + .show-hide = 显示/隐藏 + errors = .save-warning = 保存警告 .unable-to-save-drawing = 无法保存图形 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index f4efed5f4..86825c1c0 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -1858,6 +1858,15 @@ common = .revolve-created-objects-failed-sources = REVOLVE:已建立 __ocs_arg_created__ 個物件;__ocs_arg_failed__ 個來源物件無法旋轉。 .revolve-no-selected-object-could-be-revolved = REVOLVE:無法使用指定選項旋轉任何選取的物件。 + .auto-constrain = 自動約束 + .show-all = 全部顯示 + .hide-all = 全部隱藏 + .delete-constraints = 刪除約束 + .parameters-manager = 參數管理員 + .geometric = 幾何 + .dimensional = 標註 + .show-hide = 顯示/隱藏 + errors = .save-warning = 儲存警告 .unable-to-save-drawing = 無法儲存圖形 diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 05e05a4e7..dc59babfa 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -978,6 +978,267 @@ impl OpenCADStudio { } // ── Persistent constraints ──────────────────────────────────── + "GCSHOW" | "GCHIDE" | "DCSHOW" | "DCHIDE" => { + 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_sketch_scope(); + let dimensional = cmd.starts_with("DC"); + let visible = !matches!(cmd, "GCHIDE" | "DCHIDE"); + let count = self.tabs[i].scene.set_sketch_constraint_visibility( + scope, + Some(&handles), + dimensional, + visible, + ); + self.command_line.push_output( + format!("{} constraint indicator(s) updated.", count).as_str(), + ); + } + } + + "GCRESET" => { + let scope = self.tabs[i].current_sketch_scope(); + let count = self.tabs[i] + .scene + .set_sketch_constraint_visibility(scope, None, false, true); + self.command_line + .push_output(format!("{} constraint indicator(s) reset.", count).as_str()); + } + + "GCSHOWALL" | "GCHIDEALL" | "DCSHOWALL" | "DCHIDEALL" => { + let scope = self.tabs[i].current_sketch_scope(); + let dimensional = cmd.starts_with("DC"); + let visible = matches!(cmd, "GCSHOWALL" | "DCSHOWALL"); + let count = self.tabs[i].scene.set_sketch_constraint_visibility( + scope, + None, + dimensional, + visible, + ); + self.command_line + .push_output(format!("{} constraint indicator(s) updated.", count).as_str()); + } + + "DELCONSTRAINT" => { + 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_sketch_scope(); + let Some(before) = self.tabs[i] + .scene + .sketch_constraint_set(scope) + .cloned() + else { + self.command_line.push_output("No constraints found."); + return None; + }; + let mut ids: Vec<_> = before + .constraints + .iter() + .filter(|constraint| { + constraint + .refs + .iter() + .any(|reference| handles.contains(&reference.entity)) + }) + .map(|constraint| constraint.id) + .collect(); + ids.sort_unstable(); + ids.dedup(); + if ids.is_empty() { + self.command_line.push_output("No constraints found."); + return None; + } + let pending = self.begin_undo(i, "Delete constraints", handles.len(), true); + self.tabs[i] + .scene + .record_undo_sketch_constraints_before(scope, before); + let set = self.tabs[i].scene.sketch_constraint_set_mut(scope); + for id in &ids { + set.remove(*id); + } + let changes: Vec<_> = handles + .iter() + .copied() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line + .push_output(format!("{} constraint(s) deleted.", ids.len()).as_str()); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } + } + + "AUTOCONSTRAIN" => { + 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_sketch_scope(); + let inferred = self.tabs[i] + .scene + .inferred_sketch_constraints(scope, &handles); + if inferred.is_empty() { + self.command_line + .push_output("No supported geometric relations were found."); + return None; + } + let before = self.tabs[i] + .scene + .sketch_constraint_set(scope) + .cloned() + .unwrap_or_else(|| { + crate::scene::sketch_constraints::SketchConstraintSet::new(scope) + }); + let pending = self.begin_undo(i, "Auto constrain", handles.len(), true); + self.tabs[i] + .scene + .record_undo_sketch_constraints_before(scope, before); + let count = inferred.len(); + for (kind, refs) in inferred { + self.tabs[i] + .scene + .sketch_constraint_set_mut(scope) + .add(kind, refs, None); + } + let changes: Vec<_> = handles + .iter() + .copied() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i].scene.bump_entities(&changes); + self.tabs[i].dirty = true; + self.refresh_properties(); + self.command_line + .push_output(format!("{} constraint(s) applied.", count).as_str()); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } + } + + "SMOOTHCONSTRAINT" => { + 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 Some(refs) = self.tabs[i].scene.smooth_constraint_refs(&handles) else { + self.command_line.push_output( + "Select one open spline and one open target curve.", + ); + return None; + }; + use crate::command::CmdResult; + use crate::scene::sketch_constraints::ConstraintKind; + return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint { + kind: ConstraintKind::Smooth, + refs, + driving_param: None, + label: "Smooth constraint", + })); + } + } + + "DCCONVERT" => { + 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_sketch_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_sketch_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 before = self.tabs[i] + .scene + .sketch_constraint_set(scope) + .cloned() + .unwrap_or_else(|| { + crate::scene::sketch_constraints::SketchConstraintSet::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_sketch_constraints_before(scope, before); + for (_, kind, refs, value) in conversions { + self.tabs[i] + .scene + .sketch_constraint_set_mut(scope) + .add(kind, refs, Some(value)); + } + 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(); + self.command_line.push_output( + format!("{} dimension(s) converted.", dimensions.len()).as_str(), + ); + if let Some(pd) = pending { + self.commit_undo_delta(i, pd); + } + } + } + "HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { @@ -1119,7 +1380,8 @@ impl OpenCADStudio { } } - "DCONSTRAINT" => { + "DCONSTRAINT" | "DCLINEAR" | "DCHORIZONTAL" | "DCVERTICAL" | "DCALIGNED" + | "DCRADIUS" | "DCDIAMETER" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { use crate::modules::draw::select::SelectObjectsCommand; @@ -1130,8 +1392,22 @@ impl OpenCADStudio { self.command_line .push_output("Select exactly one line or circle, then run this constraint again."); } else { - use crate::modules::parametric::DistanceConstraintCommand; - match DistanceConstraintCommand::new(&self.tabs[i].scene, handles[0]) { + use crate::modules::parametric::{DistanceConstraintCommand, DistanceMode}; + let (command_name, mode) = match cmd { + "DCLINEAR" => ("DCLINEAR", DistanceMode::Linear), + "DCHORIZONTAL" => ("DCHORIZONTAL", DistanceMode::X), + "DCVERTICAL" => ("DCVERTICAL", DistanceMode::Y), + "DCALIGNED" => ("DCALIGNED", DistanceMode::Aligned), + "DCRADIUS" => ("DCRADIUS", DistanceMode::Radius), + "DCDIAMETER" => ("DCDIAMETER", DistanceMode::Diameter), + _ => ("DCONSTRAINT", DistanceMode::Auto), + }; + match DistanceConstraintCommand::with_mode( + &self.tabs[i].scene, + handles[0], + command_name, + mode, + ) { Some(new_cmd) => { self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); @@ -1141,7 +1417,7 @@ impl OpenCADStudio { } } - "ACONSTRAINT" => { + "ACONSTRAINT" | "DCANGULAR" => { let handles = self.tabs[i].scene.selected_handles_in_order(); if handles.is_empty() { use crate::modules::draw::select::SelectObjectsCommand; @@ -1154,7 +1430,17 @@ impl OpenCADStudio { ); } else { use crate::modules::parametric::AngleConstraintCommand; - match AngleConstraintCommand::new(&self.tabs[i].scene, handles[0], handles[1]) { + let command_name = if cmd == "DCANGULAR" { + "DCANGULAR" + } else { + "ACONSTRAINT" + }; + match AngleConstraintCommand::with_name( + &self.tabs[i].scene, + handles[0], + handles[1], + command_name, + ) { Some(new_cmd) => { self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index d79da3bcd..31ed12e28 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -839,6 +839,7 @@ bg={bg_ms:.1}ms n={view_count}" set.constraints .iter() .filter(|c| c.enabled) + .filter(|c| tab.scene.is_sketch_constraint_visible(scope, c.id)) .filter_map(|c| { let (anchor, outward) = crate::scene::sketch_constraints::glyph_placement( diff --git a/src/entities/spline.rs b/src/entities/spline.rs index b4fe5cc88..e68c3b38a 100644 --- a/src/entities/spline.rs +++ b/src/entities/spline.rs @@ -47,6 +47,43 @@ pub(crate) fn fit_nurbs3(spline: &Spline) -> Option { tangent(spline.end_tangent), parameterization) } +pub(crate) fn nurbs3(spline: &Spline) -> Option { + let curve = if uses_fit_method(spline) { + fit_nurbs3(spline)? + } else { + let degree = usize::try_from(spline.degree).ok()?; + let weights = if spline.weights.is_empty() { + vec![1.0; spline.control_points.len()] + } else { + spline.weights.clone() + }; + NurbsCurve3::new_strict( + degree, + spline.control_points.iter().map(|point| [point.x, point.y, point.z]).collect(), + spline.knots.clone(), + weights, + )? + }; + Some(curve.with_periodicity(spline.flags.closed || spline.flags.periodic)) +} + +pub(crate) fn replace_with_nurbs(spline: &mut Spline, curve: &NurbsCurve3) { + spline.degree = curve.degree() as i32; + spline.knots = curve.knots().to_vec(); + spline.control_points = curve.control_points().iter() + .map(|point| acadrust::types::Vector3::new(point[0], point[1], point[2])).collect(); + spline.weights = curve.weights().to_vec(); + spline.fit_points.clear(); + spline.begin_tangent = acadrust::types::Vector3::ZERO; + spline.end_tangent = acadrust::types::Vector3::ZERO; + spline.flags.rational = spline.weights.windows(2) + .any(|pair| (pair[0] - pair[1]).abs() > 1e-12); + spline.flags.closed = false; + spline.flags.periodic = false; + spline.dwg_flags1 &= !1; + spline.dxf_flags &= !(32 | 1024); +} + /// Conservative world bounds of a fit-method spline. Fit points are interpolation /// constraints, not control-hull vertices, so their box can exclude the curve. /// The shared kernel reconstructs the same spatial interpolation used by the diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 8714b1dc2..fa1dd3a69 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -3,6 +3,14 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static str)> { match source { + "Auto Constrain" => Some(("common", "auto-constrain")), + "Delete Constraints" => Some(("common", "delete-constraints")), + "Dimensional" => Some(("common", "dimensional")), + "Geometric" => Some(("common", "geometric")), + "Hide All" => Some(("common", "hide-all")), + "Parameters Manager" => Some(("common", "parameters-manager")), + "Show All" => Some(("common", "show-all")), + "Show/Hide" => Some(("common", "show-hide")), "Boolean" => Some(("model", "boolean")), "Create" => Some(("model", "create")), "Extrude" => Some(("model", "extrude")), diff --git a/src/modules/parametric/mod.rs b/src/modules/parametric/mod.rs index 8febe3272..9cbcc2910 100644 --- a/src/modules/parametric/mod.rs +++ b/src/modules/parametric/mod.rs @@ -16,7 +16,10 @@ pub use tools::{ colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric, tangent, vertical, }; -pub use value::{angle_tool, distance_tool, AngleConstraintCommand, DistanceConstraintCommand}; +pub use value::{ + angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand, + DistanceConstraintCommand, DistanceMode, +}; use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef}; @@ -34,68 +37,107 @@ impl CadModule for ParametricModule { fn ribbon_groups(&self) -> &[RibbonGroup] { static GROUPS: std::sync::OnceLock> = std::sync::OnceLock::new(); GROUPS.get_or_init(|| { - let extra_constraints = [ - normal::tool(), - center_point_tool::tool(), - midpoint_tool::tool(), - point_on_curve_tool::tool(), - equal_distance_tool::tool(), - ]; + let command = |id: &'static str, label: &'static str, icon: &'static [u8]| ToolDef { + id, label, icon: IconKind::Svg(icon), event: ModuleEvent::Command(id.to_string()), + }; vec![ RibbonGroup { - title: "Geometry", + title: "Geometric", tools: vec![ - coincident_tool::tool().into(), - parallel::tool().into(), - tangent::tool().into(), - colinear::tool().into(), - perpendicular::tool().into(), - RibbonItem::Dropdown { - id: "PARAMETRIC_MORE", - icon: extra_constraints[0].icon, - items: extra_constraints - .iter() - .map(|tool| (tool.id, tool.label, tool.icon)) - .collect(), - default: "NRCONSTRAINT", + RibbonItem::LargeTool(command( + "AUTOCONSTRAIN", "Auto Constrain", + include_bytes!("../../../assets/icons/constrain/auto.svg"), + )), + RibbonItem::ToolGrid { columns: vec![ + vec![coincident_tool::tool(), parallel::tool(), tangent::tool()], + vec![ + colinear::tool(), perpendicular::tool(), + command("SMOOTHCONSTRAINT", "Smooth", include_bytes!("../../../assets/icons/constrain/smooth.svg")), + ], + vec![concentric::tool(), horizontal::tool(), symmetric::tool()], + vec![fixed::tool(), vertical::tool(), equal::tool()], + ] }, + RibbonItem::LabeledDropdown { + id: "GCVISIBILITY", label: "Show/Hide", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")), + items: vec![ + ("GCSHOW", "Show", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg"))), + ("GCHIDE", "Hide", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/hide_all.svg"))), + ("GCRESET", "Reset", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg"))), + ], default: "GCSHOW", }, - concentric::tool().into(), - horizontal::tool().into(), - symmetric::tool().into(), - fixed::tool().into(), - vertical::tool().into(), - equal::tool().into(), + RibbonItem::LabeledTool(command( + "GCSHOWALL", "Show All", include_bytes!("../../../assets/icons/constrain/show_all.svg"), + )), + RibbonItem::LabeledTool(command( + "GCHIDEALL", "Hide All", include_bytes!("../../../assets/icons/constrain/hide_all.svg"), + )), ], }, RibbonGroup { - title: "Dimension", + title: "Dimensional", tools: vec![ - RibbonItem::LargeTool(distance_tool::tool()), - RibbonItem::LargeTool(angle_tool::tool()), + RibbonItem::LargeDropdown { + id: "DC_LINEAR_MENU", label: "Linear", icon: dimensional_tools::linear().icon, + items: [dimensional_tools::linear(), dimensional_tools::horizontal(), dimensional_tools::vertical()] + .iter().map(|tool| (tool.id, tool.label, tool.icon)).collect(), + default: "DCLINEAR", + }, + RibbonItem::LargeTool(dimensional_tools::aligned()), + RibbonItem::ToolGrid { columns: vec![ + vec![dimensional_tools::angular(), dimensional_tools::diameter()], + vec![dimensional_tools::radius(), dimensional_tools::convert()], + ] }, + RibbonItem::LabeledDropdown { + id: "DCVISIBILITY", label: "Show/Hide", + icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")), + items: vec![ + ("DCSHOW", "Show", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg"))), + ("DCHIDE", "Hide", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/hide_all.svg"))), + ], default: "DCSHOW", + }, + RibbonItem::LabeledTool(command( + "DCSHOWALL", "Show All", include_bytes!("../../../assets/icons/constrain/show_all.svg"), + )), + RibbonItem::LabeledTool(command( + "DCHIDEALL", "Hide All", include_bytes!("../../../assets/icons/constrain/hide_all.svg"), + )), ], }, RibbonGroup { title: "Manage", - tools: vec![RibbonItem::LargeTool(ToolDef { - id: "PARAMETERS", - label: "Parameters", - icon: IconKind::Glyph("ƒ"), - event: ModuleEvent::Command("PARAMETERS".to_string()), - })], + tools: vec![ + RibbonItem::LargeTool(command( + "DELCONSTRAINT", "Delete Constraints", include_bytes!("../../../assets/icons/constrain/delete.svg"), + )), + RibbonItem::LargeTool(command( + "PARAMETERS", "Parameters Manager", include_bytes!("../../../assets/icons/constrain/parameters.svg"), + )), + ], }, ] }) } } +inventory::submit!(crate::command::CommandRegistration { + names: &[ + "AUTOCONSTRAIN", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET", + "GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL", + "DCCONVERT", "DELCONSTRAINT", + ] +}); + #[cfg(test)] mod tests { use super::*; fn item_id(item: &RibbonItem) -> &'static str { match item { - RibbonItem::Tool(tool) | RibbonItem::LargeTool(tool) => tool.id, - RibbonItem::Dropdown { id, .. } | RibbonItem::LargeDropdown { id, .. } => id, + RibbonItem::Tool(tool) | RibbonItem::LabeledTool(tool) | RibbonItem::LargeTool(tool) => tool.id, + RibbonItem::Dropdown { id, .. } | RibbonItem::LabeledDropdown { id, .. } + | RibbonItem::LargeDropdown { id, .. } => id, + RibbonItem::ToolGrid { .. } => "GRID", _ => panic!("unexpected composite ribbon item"), } } @@ -106,43 +148,34 @@ mod tests { assert_eq!( groups.iter().map(|group| group.title).collect::>(), - ["Geometry", "Dimension", "Manage"] + ["Geometric", "Dimensional", "Manage"] ); assert_eq!( groups[0].tools.iter().map(item_id).collect::>(), - [ - "CCONSTRAINT", - "PCONSTRAINT", - "TCONSTRAINT", - "LCONSTRAINT", - "QCONSTRAINT", - "PARAMETRIC_MORE", - "NCONSTRAINT", - "HCONSTRAINT", - "SYCONSTRAINT", - "FXCONSTRAINT", - "VCONSTRAINT", - "ECONSTRAINT", - ] + ["AUTOCONSTRAIN", "GRID", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL"] ); assert_eq!( groups[1].tools.iter().map(item_id).collect::>(), - ["DCONSTRAINT", "ACONSTRAINT"] + ["DC_LINEAR_MENU", "DCALIGNED", "GRID", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL"] ); - assert_eq!(item_id(&groups[2].tools[0]), "PARAMETERS"); + assert_eq!(groups[2].tools.iter().map(item_id).collect::>(), ["DELCONSTRAINT", "PARAMETERS"]); - let RibbonItem::Dropdown { items, .. } = &groups[0].tools[5] else { - panic!("additional geometric constraints must stay in a dropdown"); + let RibbonItem::LargeDropdown { items, default, .. } = &groups[1].tools[0] else { + panic!("linear dimensional constraint must be a split dropdown"); }; + assert_eq!(*default, "DCLINEAR"); assert_eq!( items.iter().map(|(id, _, _)| *id).collect::>(), - [ - "NRCONSTRAINT", - "CPCONSTRAINT", - "MPCONSTRAINT", - "OCCONSTRAINT", - "EDCONSTRAINT", - ] + ["DCLINEAR", "DCHORIZONTAL", "DCVERTICAL"] ); + let RibbonItem::ToolGrid { columns } = &groups[0].tools[1] else { panic!("geometry grid") }; + assert_eq!(columns.iter().flatten().map(|tool| tool.id).collect::>(), [ + "CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT", "LCONSTRAINT", "QCONSTRAINT", "SMOOTHCONSTRAINT", + "NCONSTRAINT", "HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT", "ECONSTRAINT", + ]); + let RibbonItem::ToolGrid { columns } = &groups[1].tools[2] else { panic!("dimension grid") }; + assert_eq!(columns.iter().flatten().map(|tool| tool.id).collect::>(), [ + "DCANGULAR", "DCDIAMETER", "DCRADIUS", "DCCONVERT", + ]); } } diff --git a/src/modules/parametric/value.rs b/src/modules/parametric/value.rs index 71e741351..068923701 100644 --- a/src/modules/parametric/value.rs +++ b/src/modules/parametric/value.rs @@ -60,6 +60,23 @@ pub mod distance_tool { } } +pub mod dimensional_tools { + use super::*; + + fn command(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef { + ToolDef { id, label, icon: IconKind::Svg(icon), event: ModuleEvent::Command(id.to_string()) } + } + + pub fn linear() -> ToolDef { command("DCLINEAR", "Linear", include_bytes!("../../../assets/icons/dim_linear.svg")) } + pub fn horizontal() -> ToolDef { command("DCHORIZONTAL", "Horizontal", include_bytes!("../../../assets/icons/constrain/distance_x.svg")) } + pub fn vertical() -> ToolDef { command("DCVERTICAL", "Vertical", include_bytes!("../../../assets/icons/constrain/distance_y.svg")) } + pub fn aligned() -> ToolDef { command("DCALIGNED", "Aligned", include_bytes!("../../../assets/icons/dim_aligned.svg")) } + pub fn angular() -> ToolDef { command("DCANGULAR", "Angular", include_bytes!("../../../assets/icons/dim_angular.svg")) } + pub fn radius() -> ToolDef { command("DCRADIUS", "Radius", include_bytes!("../../../assets/icons/dim_radius.svg")) } + pub fn diameter() -> ToolDef { command("DCDIAMETER", "Diameter", include_bytes!("../../../assets/icons/dim_diameter.svg")) } + pub fn convert() -> ToolDef { command("DCCONVERT", "Convert", include_bytes!("../../../assets/icons/constrain/convert.svg")) } +} + pub mod angle_tool { use super::*; pub fn tool() -> ToolDef { @@ -76,10 +93,14 @@ pub mod angle_tool { /// Which flavor of dimensional constraint the typed value drives. #[derive(Clone, Copy, PartialEq, Eq)] -enum DistanceMode { +pub enum DistanceMode { /// Line length (two-point distance) or a circle/arc's radius — /// whichever `is_circle` selects. Auto, + /// Chooses the dominant world-axis component. + Linear, + Aligned, + Radius, X, Y, Diameter, @@ -92,6 +113,7 @@ enum DistanceMode { /// its two endpoints, or a circle/arc's radius (or diameter, or — for an /// arc specifically — its swept arc length), to a typed target value. pub struct DistanceConstraintCommand { + command_name: &'static str, handle: Handle, /// Whether `handle` is a circle/arc (Radius/Diameter) or a line /// (Distance/DistanceX/DistanceY) — decided once at construction from @@ -115,6 +137,15 @@ pub struct DistanceConstraintCommand { impl DistanceConstraintCommand { /// `None` if `handle` isn't a Line, Circle, or Arc. pub fn new(scene: &Scene, handle: Handle) -> Option { + Self::with_mode(scene, handle, "DCONSTRAINT", DistanceMode::Auto) + } + + pub fn with_mode( + scene: &Scene, + handle: Handle, + command_name: &'static str, + requested_mode: DistanceMode, + ) -> Option { let entity = scene.document.get_entity(handle)?; let (is_circle, is_arc, default_value, default_x, default_y, default_arc_length) = match entity { @@ -132,11 +163,23 @@ impl DistanceConstraintCommand { .iter() .map(|p| p.name.clone()) .collect(); + let mode = match requested_mode { + DistanceMode::Linear if !is_circle => { + if default_x.abs() >= default_y.abs() { DistanceMode::X } else { DistanceMode::Y } + } + DistanceMode::Aligned if !is_circle => DistanceMode::Aligned, + DistanceMode::Radius if is_circle => DistanceMode::Radius, + DistanceMode::Diameter if is_circle => DistanceMode::Diameter, + DistanceMode::X | DistanceMode::Y if !is_circle => requested_mode, + DistanceMode::Auto => DistanceMode::Auto, + _ => return None, + }; Some(Self { + command_name, handle, is_circle, is_arc, - mode: DistanceMode::Auto, + mode, default_value, default_x, default_y, @@ -163,7 +206,7 @@ impl DistanceConstraintCommand { vec![SketchRef::whole(self.handle)], "Arc length constraint", ), - (true, _) => ( + (true, DistanceMode::Auto | DistanceMode::Radius) => ( ConstraintKind::Radius, vec![SketchRef::whole(self.handle)], "Radius constraint", @@ -184,7 +227,7 @@ impl DistanceConstraintCommand { ], "DistanceY constraint", ), - (false, _) => ( + (false, DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear) => ( ConstraintKind::Distance, vec![ SketchRef::point(self.handle, 0), @@ -192,6 +235,7 @@ impl DistanceConstraintCommand { ], "Distance constraint", ), + _ => return None, }; Some(CmdResult::AddSketchConstraint { kind, @@ -204,7 +248,7 @@ impl DistanceConstraintCommand { impl CadCommand for DistanceConstraintCommand { fn name(&self) -> &'static str { - "DCONSTRAINT" + self.command_name } fn prompt(&self) -> String { @@ -219,20 +263,21 @@ impl CadCommand for DistanceConstraintCommand { self.default_arc_length ) } - (true, _) if self.is_arc => format!( + (true, DistanceMode::Auto | DistanceMode::Radius) if self.is_arc => format!( "Specify distance <{:.4}> or [Diameter/Arclength]: ", self.default_value ), - (true, _) => format!( + (true, DistanceMode::Auto | DistanceMode::Radius) => format!( "Specify distance <{:.4}> or [Diameter]: ", self.default_value ), (false, DistanceMode::X) => format!("Specify X distance <{:.4}>: ", self.default_x), (false, DistanceMode::Y) => format!("Specify Y distance <{:.4}>: ", self.default_y), - (false, _) => format!( + (false, DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear) => format!( "Specify distance <{:.4}> or [Xdistance/Ydistance]: ", self.default_value ), + _ => "Unsupported distance constraint mode.".to_string(), } } @@ -247,10 +292,10 @@ impl CadCommand for DistanceConstraintCommand { fn on_enter(&mut self) -> CmdResult { let default = match self.mode { DistanceMode::Diameter => self.default_value * 2.0, - DistanceMode::ArcLength if self.is_arc => self.default_arc_length, + DistanceMode::ArcLength => self.default_arc_length, DistanceMode::X => self.default_x, DistanceMode::Y => self.default_y, - _ => self.default_value, + DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear | DistanceMode::Radius => self.default_value, }; self.build(DrivingValue::Literal(default)) .unwrap_or(CmdResult::Cancel) @@ -274,7 +319,7 @@ impl CadCommand for DistanceConstraintCommand { return Some(CmdResult::NeedPoint); } (true, "R" | "RADIUS") => { - self.mode = DistanceMode::Auto; + self.mode = DistanceMode::Radius; return Some(CmdResult::NeedPoint); } (false, "X" | "XDISTANCE") => { @@ -298,6 +343,7 @@ impl CadCommand for DistanceConstraintCommand { /// Constrains the angle (in degrees) from `fixed`'s direction to `moving`'s. pub struct AngleConstraintCommand { + command_name: &'static str, fixed_handle: Handle, moving_handle: Handle, default_value: f64, @@ -309,6 +355,15 @@ pub struct AngleConstraintCommand { impl AngleConstraintCommand { /// `None` unless both `fixed` and `moving` are lines. pub fn new(scene: &Scene, fixed: Handle, moving: Handle) -> Option { + Self::with_name(scene, fixed, moving, "ACONSTRAINT") + } + + pub fn with_name( + scene: &Scene, + fixed: Handle, + moving: Handle, + command_name: &'static str, + ) -> Option { let fixed_entity = scene.document.get_entity(fixed)?; let moving_entity = scene.document.get_entity(moving)?; let (EntityType::Line(f), EntityType::Line(m)) = (fixed_entity, moving_entity) else { @@ -323,6 +378,7 @@ impl AngleConstraintCommand { .map(|p| p.name.clone()) .collect(); Some(Self { + command_name, fixed_handle: fixed, moving_handle: moving, default_value, @@ -348,7 +404,7 @@ impl AngleConstraintCommand { impl CadCommand for AngleConstraintCommand { fn name(&self) -> &'static str { - "ACONSTRAINT" + self.command_name } fn prompt(&self) -> String { @@ -380,7 +436,10 @@ impl CadCommand for AngleConstraintCommand { // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { - names: &["DCONSTRAINT", "ACONSTRAINT"] + names: &[ + "DCONSTRAINT", "ACONSTRAINT", "DCLINEAR", "DCHORIZONTAL", "DCVERTICAL", + "DCALIGNED", "DCANGULAR", "DCRADIUS", "DCDIAMETER", + ] }); #[cfg(test)] diff --git a/src/scene/dimension_assoc.rs b/src/scene/dimension_assoc.rs index beb62e80a..8069e6eea 100644 --- a/src/scene/dimension_assoc.rs +++ b/src/scene/dimension_assoc.rs @@ -275,6 +275,12 @@ pub(crate) fn source_points(entity: &EntityType) -> Vec { EntityType::Line(line) => vec![line.start, line.end], EntityType::Arc(arc) => vec![arc.start_point_wcs(), arc.end_point_wcs()], EntityType::Circle(_) => Vec::new(), + EntityType::Spline(spline) => crate::entities::spline::nurbs3(spline) + .map(|curve| { + let (start, end) = curve.domain(); + [curve.point_at_knot(start), curve.point_at_knot(end)].into_iter() + .map(|point| Vector3::new(point[0], point[1], point[2])).collect() + }).unwrap_or_default(), EntityType::LwPolyline(polyline) => polyline .vertices .iter() @@ -564,6 +570,59 @@ pub(crate) fn dimension_is_associative( }) } +pub(crate) fn constraint_from_associative_dimension( + document: &acadrust::CadDocument, + handle: Handle, +) -> Option<( + super::sketch_constraints::ConstraintKind, + Vec, + super::named_parameters::DrivingValue, +)> { + use super::named_parameters::DrivingValue; + use super::sketch_constraints::{ConstraintKind, SketchRef}; + + let EntityType::Dimension(dimension) = document.get_entity(handle)? else { return None }; + let association = document.objects.values().find_map(|object| { + let ObjectType::Associative(object) = object else { return None }; + let AssociativeData::DimensionAssociation(association) = &object.data else { return None }; + (association.dimension == handle && association.associativity != 0).then_some(association) + })?; + let point = |index: usize| { + let reference = association.references.get(index)?.first()?; + Some(SketchRef::point(*reference.xrefs.first()?, reference.main_gs_marker)) + }; + let whole = |index: usize| { + let reference = association.references.get(index)?.first()?; + Some(SketchRef::whole(*reference.xrefs.first()?)) + }; + let measurement = DrivingValue::Literal(dimension.measurement()); + match dimension { + Dimension::Aligned(_) => Some((ConstraintKind::Distance, vec![point(0)?, point(1)?], measurement)), + Dimension::Linear(linear) => { + let direction = [linear.rotation.cos().abs(), linear.rotation.sin().abs()]; + let kind = if direction[1] <= 1e-9 { ConstraintKind::DistanceX } + else if direction[0] <= 1e-9 { ConstraintKind::DistanceY } + else { return None }; + Some((kind, vec![point(0)?, point(1)?], measurement)) + } + Dimension::Radius(_) | Dimension::LargeRadial(_) => { + Some((ConstraintKind::Radius, vec![whole(0)?], measurement)) + } + Dimension::Diameter(_) => Some((ConstraintKind::Diameter, vec![whole(0)?], measurement)), + Dimension::Angular2Ln(_) => { + let mut sources: Vec = association.references.iter().flatten() + .filter_map(|reference| reference.xrefs.first().copied()).collect(); + sources.sort_unstable(); + sources.dedup(); + let [first, second] = sources.as_slice() else { return None }; + Some((ConstraintKind::Angle, + vec![SketchRef::whole(*first), SketchRef::whole(*second)], measurement)) + } + Dimension::Arc(_) => Some((ConstraintKind::ArcLength, vec![whole(0)?], measurement)), + Dimension::Angular3Pt(_) | Dimension::Ordinate(_) => None, + } +} + pub(crate) fn radial_extension_points( document: &acadrust::CadDocument, dimension: Handle, diff --git a/src/scene/dwg_native_constraints.rs b/src/scene/dwg_native_constraints.rs index 56b8047ab..852315988 100644 --- a/src/scene/dwg_native_constraints.rs +++ b/src/scene/dwg_native_constraints.rs @@ -250,6 +250,7 @@ fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> { ConstraintKind::Vertical => Some("ACVERTICALCONSTRAINT"), ConstraintKind::Perpendicular => Some("ACPERPENDICULARCONSTRAINT"), ConstraintKind::Tangent => Some("ACTANGENTCONSTRAINT"), + ConstraintKind::Smooth => Some("ACG2SMOOTHCONSTRAINT"), // the native format's own `GeomConstraintType` enum (native SDK's // `AcGeomConstraint.h`) lists `kNormal` alongside `kPerpendicular` // as a distinct, real geometric-constraint kind. diff --git a/src/scene/mod.rs b/src/scene/mod.rs index f42a4562c..d36fef59f 100644 --- a/src/scene/mod.rs +++ b/src/scene/mod.rs @@ -1927,6 +1927,11 @@ pub struct Scene { /// Persistent parametric constraint sets, one per sketch scope. /// Materialized into scoped XRecords during save and restored on open. pub(crate) sketch_constraints: Vec, + /// Session-only visibility overrides for constraint glyphs. + hidden_sketch_constraints: HashSet<( + sketch_constraints::SketchScope, + sketch_constraints::ConstraintId, + )>, /// Document-wide named-parameter and expression table. /// Persisted through a single drawing XRecord. pub(crate) named_parameters: named_parameters::ParameterTable, @@ -2244,6 +2249,7 @@ impl Scene { dependency_index_cache: RefCell::new(None), associative_hatch_source_cache: RefCell::new(None), sketch_constraints: Vec::new(), + hidden_sketch_constraints: HashSet::default(), named_parameters: named_parameters::ParameterTable::new(), has_associative_centers: std::cell::Cell::new(None), block_defn_cache: RefCell::new(HashMap::default()), diff --git a/src/scene/sketch_constraints.rs b/src/scene/sketch_constraints.rs index 202f1f76b..cdbb50c9f 100644 --- a/src/scene/sketch_constraints.rs +++ b/src/scene/sketch_constraints.rs @@ -77,6 +77,8 @@ pub enum ConstraintKind { Angle, Radius, Tangent, + /// An open spline endpoint remains curvature-continuous with another curve endpoint. + Smooth, /// Two circles/arcs share a center — `refs`: `[center(a), center(b)]`. /// Solves identically to `Coincident` (`sketch_solve.rs` broadens that /// match arm rather than duplicating it) — only the DWG-native class @@ -359,6 +361,7 @@ impl ConstraintKind { ConstraintKind::Angle => "∠", ConstraintKind::Radius => "R", ConstraintKind::Tangent => "T", + ConstraintKind::Smooth => "G²", ConstraintKind::Concentric => "◎", ConstraintKind::CenterPoint => "⊕", ConstraintKind::Colinear => "L", @@ -467,6 +470,136 @@ pub(crate) fn glyph_placement( } impl super::Scene { + pub fn is_sketch_constraint_visible(&self, scope: SketchScope, id: ConstraintId) -> bool { + !self.hidden_sketch_constraints.contains(&(scope, id)) + } + + pub fn set_sketch_constraint_visibility( + &mut self, + scope: SketchScope, + handles: Option<&[Handle]>, + dimensional: bool, + visible: bool, + ) -> usize { + let ids: Vec<_> = self.sketch_constraint_set(scope).into_iter() + .flat_map(|set| set.constraints.iter()) + .filter(|constraint| constraint.driving_param.is_some() == dimensional) + .filter(|constraint| handles.is_none_or(|handles| constraint.refs.iter() + .any(|reference| handles.contains(&reference.entity)))) + .map(|constraint| constraint.id).collect(); + for id in &ids { + if visible { self.hidden_sketch_constraints.remove(&(scope, *id)); } + else { self.hidden_sketch_constraints.insert((scope, *id)); } + } + ids.len() + } + + /// Infers relations already present in the selected geometry. + pub fn inferred_sketch_constraints( + &self, + scope: SketchScope, + handles: &[Handle], + ) -> Vec<(ConstraintKind, Vec)> { + use cadkernel::geom2d::{ + infer_constraints, Arc, Circle, ConstraintEndpoint, InferredConstraint, Line, + SketchPrimitive, Tolerance, + }; + let mut sources = Vec::new(); + for handle in handles { + let primitive = match self.document.get_entity(*handle) { + Some(acadrust::EntityType::Line(line)) => SketchPrimitive::Line(Line { + start: [line.start.x, line.start.y], end: [line.end.x, line.end.y], + }), + Some(acadrust::EntityType::Circle(circle)) => SketchPrimitive::Circle(Circle { + centre: [circle.center.x, circle.center.y], radius: circle.radius, + }), + Some(acadrust::EntityType::Arc(arc)) => SketchPrimitive::Arc(Arc { + centre: [arc.center.x, arc.center.y], radius: arc.radius, + start_angle: arc.start_angle, end_angle: arc.end_angle, + }), + _ => continue, + }; + sources.push((*handle, primitive)); + } + let primitives: Vec<_> = sources.iter().map(|(_, primitive)| *primitive).collect(); + let marker = |endpoint| match endpoint { + ConstraintEndpoint::Start => 0, + ConstraintEndpoint::End => 1, + }; + let mut mapped: Vec<_> = infer_constraints( + &primitives, Tolerance::new(1e-6), 0.5_f64.to_radians(), + ).into_iter().map(|relation| match relation { + InferredConstraint::Coincident { first, first_endpoint, second, second_endpoint } => ( + ConstraintKind::Coincident, + vec![SketchRef::point(sources[first].0, marker(first_endpoint)), + SketchRef::point(sources[second].0, marker(second_endpoint))], + ), + InferredConstraint::Collinear { first, second } => ( + ConstraintKind::Colinear, + vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)], + ), + InferredConstraint::Concentric { first, second } => ( + ConstraintKind::Concentric, + vec![SketchRef::center(sources[first].0), SketchRef::center(sources[second].0)], + ), + InferredConstraint::Parallel { first, second } => ( + ConstraintKind::Parallel, + vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)], + ), + InferredConstraint::Perpendicular { first, second } => ( + ConstraintKind::Perpendicular, + vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)], + ), + InferredConstraint::Horizontal { entity } => ( + ConstraintKind::Horizontal, vec![SketchRef::whole(sources[entity].0)], + ), + InferredConstraint::Vertical { entity } => ( + ConstraintKind::Vertical, vec![SketchRef::whole(sources[entity].0)], + ), + InferredConstraint::Tangent { first, second } => ( + ConstraintKind::Tangent, + vec![SketchRef::whole(sources[first].0), SketchRef::whole(sources[second].0)], + ), + }).collect(); + if let Some(existing) = self.sketch_constraint_set(scope) { + mapped.retain(|(kind, refs)| !existing.constraints.iter().any(|constraint| { + constraint.kind == *kind && (constraint.refs == *refs + || (constraint.refs.len() == 2 && refs.len() == 2 + && constraint.refs[0] == refs[1] && constraint.refs[1] == refs[0])) + })); + } + mapped.retain(|(kind, refs)| self.validate_sketch_constraint(*kind, refs, None).is_ok()); + mapped + } + + pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option> { + let [first, second] = handles else { return None }; + let first_entity = self.document.get_entity(*first)?; + let second_entity = self.document.get_entity(*second)?; + let (spline_handle, spline, target_handle, target) = match (first_entity, second_entity) { + (acadrust::EntityType::Spline(spline), target) => (*first, spline, *second, target), + (target, acadrust::EntityType::Spline(spline)) => (*second, spline, *first, target), + _ => return None, + }; + if spline.flags.closed || spline.flags.periodic { return None; } + let spline_points = super::dimension_assoc::source_points( + &acadrust::EntityType::Spline(spline.clone())); + let target_points = super::dimension_assoc::source_points(target); + let spline_ends = [*spline_points.first()?, *spline_points.last()?]; + let target_ends = [*target_points.first()?, *target_points.last()?]; + let mut best = (f64::INFINITY, 0, 0); + for (source_marker, source) in spline_ends.iter().enumerate() { + for (target_marker, target) in target_ends.iter().enumerate() { + let distance = (*source - *target).length_squared(); + if distance < best.0 { best = (distance, source_marker, target_marker); } + } + } + Some(vec![ + SketchRef::point(spline_handle, best.1 as i32), + SketchRef::point(target_handle, best.2 as i32), + ]) + } + /// The constraint set for `scope`, if one has been created. pub fn sketch_constraint_set(&self, scope: SketchScope) -> Option<&SketchConstraintSet> { self.sketch_constraints.iter().find(|s| s.scope == scope) @@ -863,4 +996,76 @@ mod tests { assert_eq!(usage.len(), 1); assert_eq!(usage[0].entities, vec![h(1)]); } + + #[test] + fn automatic_inference_maps_relations_and_skips_existing_constraints() { + let mut scene = super::super::Scene::new(); + let first = scene.add_entity(acadrust::EntityType::Line( + acadrust::entities::Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(5.0, 0.0, 0.0), + ), + )); + let second = scene.add_entity(acadrust::EntityType::Line( + acadrust::entities::Line::from_points( + Vector3::new(5.0, 0.0, 0.0), + Vector3::new(10.0, 0.0, 0.0), + ), + )); + scene + .sketch_constraint_set_mut(SketchScope::ModelSpace) + .add( + ConstraintKind::Horizontal, + vec![SketchRef::whole(first)], + None, + ); + + let inferred = scene.inferred_sketch_constraints( + SketchScope::ModelSpace, + &[first, second], + ); + + assert!(!inferred.iter().any(|(kind, refs)| { + *kind == ConstraintKind::Horizontal && *refs == [SketchRef::whole(first)] + })); + assert!(inferred.iter().any(|(kind, refs)| { + *kind == ConstraintKind::Horizontal && *refs == [SketchRef::whole(second)] + })); + assert!(inferred.iter().any(|(kind, _)| *kind == ConstraintKind::Coincident)); + assert!(inferred.iter().any(|(kind, _)| *kind == ConstraintKind::Colinear)); + } + + #[test] + fn visibility_toggles_geometric_and_dimensional_independently() { + let mut scene = super::super::Scene::new(); + let line = scene.add_entity(acadrust::EntityType::Line( + acadrust::entities::Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(5.0, 0.0, 0.0), + ), + )); + let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace); + let geometric = set.add( + ConstraintKind::Horizontal, + vec![SketchRef::whole(line)], + None, + ); + let dimensional = set.add( + ConstraintKind::Distance, + vec![SketchRef::point(line, 0), SketchRef::point(line, 1)], + Some(DrivingValue::Literal(5.0)), + ); + + assert_eq!( + scene.set_sketch_constraint_visibility( + SketchScope::ModelSpace, + None, + false, + false, + ), + 1 + ); + assert!(!scene.is_sketch_constraint_visible(SketchScope::ModelSpace, geometric)); + assert!(scene.is_sketch_constraint_visible(SketchScope::ModelSpace, dimensional)); + } } diff --git a/src/scene/sketch_solve.rs b/src/scene/sketch_solve.rs index 75e867cbb..1562a093c 100644 --- a/src/scene/sketch_solve.rs +++ b/src/scene/sketch_solve.rs @@ -233,6 +233,40 @@ fn entity_plane_z(entity: &EntityType) -> Option { { ellipse.center.z } + EntityType::Spline(spline) if !spline.flags.closed && !spline.flags.periodic => { + let curve = crate::entities::spline::nurbs3(spline)?; + let first = *curve.control_points().first()?; + if !curve + .control_points() + .iter() + .all(|point| (point[2] - first[2]).abs() <= PLANE_EPS) + { + return None; + } + first[2] + } + EntityType::LwPolyline(polyline) if is_world_z(polyline.normal) => { + let points = super::dimension_assoc::source_points(entity); + let first = *points.first()?; + if !points + .iter() + .all(|point| (point.z - first.z).abs() <= PLANE_EPS) + { + return None; + } + first.z + } + EntityType::Polyline2D(polyline) if is_world_z(polyline.normal) => { + let points = super::dimension_assoc::source_points(entity); + let first = *points.first()?; + if !points + .iter() + .all(|point| (point.z - first.z).abs() <= PLANE_EPS) + { + return None; + } + first.z + } _ => return None, }; z.is_finite().then_some(z) @@ -737,6 +771,9 @@ fn build_constraint( _ => Vec::new(), } } + // Smooth edits the endpoint controls through cadkernel's spatial + // NURBS operation after the ordinary 2D system has settled. + ConstraintKind::Smooth => Vec::new(), // A circle's radius is always normal to its own tangent, so // "line normal to circle/arc" reduces to "line passes through the // circle's center" — the same `PointOnLine` primitive @@ -778,6 +815,85 @@ fn build_constraint( } } +fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option { + let parameter = match marker { + 0 => 0.0, + 1 => 1.0, + _ => return None, + }; + let curve = crate::entities::curve::entity_curve(entity)?; + if curve.is_closed() { + return None; + } + let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 }; + let point = curve.point_at(parameter); + let tangent = curve.tangent_at(parameter); + let next = curve.point_at(parameter + step); + let third = curve.point_at(parameter + 2.0 * step); + Some(cadkernel::space::CurveJet { + point, + tangent, + curvature: cadkernel::space::curve::curvature_through(point, next, third), + }) +} + +fn apply_smooth_constraints( + document: &acadrust::CadDocument, + set: &SketchConstraintSet, + results: &mut Vec<(Handle, EntityType)>, +) { + for constraint in set + .constraints + .iter() + .filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth) + { + let [source_ref, target_ref] = constraint.refs.as_slice() else { + continue; + }; + let source = results + .iter() + .find(|(handle, _)| *handle == source_ref.entity) + .map(|(_, entity)| entity.clone()) + .or_else(|| document.get_entity(source_ref.entity).cloned()); + let target = results + .iter() + .find(|(handle, _)| *handle == target_ref.entity) + .map(|(_, entity)| entity.clone()) + .or_else(|| document.get_entity(target_ref.entity).cloned()); + let (Some(EntityType::Spline(mut spline)), Some(target)) = (source, target) else { + continue; + }; + let endpoint = match source_ref.marker { + Some(0) => cadkernel::space::SplineEnd::Start, + Some(1) => cadkernel::space::SplineEnd::End, + _ => continue, + }; + let Some(target_marker) = target_ref.marker else { + continue; + }; + let Some(target_jet) = smooth_target_jet(&target, target_marker) else { + continue; + }; + let Some(curve) = crate::entities::spline::nurbs3(&spline) else { + continue; + }; + let Some(smoothed) = cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet) + else { + continue; + }; + crate::entities::spline::replace_with_nurbs(&mut spline, &smoothed); + let updated = EntityType::Spline(spline); + if let Some((_, entity)) = results + .iter_mut() + .find(|(handle, _)| *handle == source_ref.entity) + { + *entity = updated; + } else { + results.push((source_ref.entity, updated)); + } + } +} + const MOVE_EPS: f64 = 1e-9; type SolveResult = ( Vec<(Handle, EntityType)>, @@ -823,7 +939,11 @@ fn solve_scope( } } - if cache.is_empty() { + let has_smooth = set + .constraints + .iter() + .any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth); + if cache.is_empty() && !has_smooth { return None; } @@ -1065,6 +1185,7 @@ fn solve_scope( _ => {} } } + apply_smooth_constraints(document, set, &mut results); Some((results, dof, conflicts)) } @@ -1082,6 +1203,42 @@ impl Scene { "Constraint references must be supported entities on the same world-XY plane.", ); } + if kind == ConstraintKind::Smooth { + let [source_ref, target_ref] = refs else { + return Err("Smooth requires one spline endpoint and one target endpoint."); + }; + if source_ref.entity == target_ref.entity { + return Err("Smooth requires two different entities."); + } + let Some(EntityType::Spline(spline)) = self.document.get_entity(source_ref.entity) + else { + return Err("Smooth requires an open spline as its first reference."); + }; + if spline.flags.closed || spline.flags.periodic { + return Err("Smooth requires an open spline."); + } + let endpoint = match source_ref.marker { + Some(0) => cadkernel::space::SplineEnd::Start, + Some(1) => cadkernel::space::SplineEnd::End, + _ => return Err("Smooth requires a spline endpoint."), + }; + let Some(target_marker) = target_ref.marker else { + return Err("Smooth requires a target endpoint."); + }; + let Some(target) = self.document.get_entity(target_ref.entity) else { + return Err("Smooth target does not exist."); + }; + let Some(target_jet) = smooth_target_jet(target, target_marker) else { + return Err("The selected target does not support endpoint smoothing."); + }; + let Some(curve) = crate::entities::spline::nurbs3(spline) else { + return Err("The selected spline cannot be evaluated."); + }; + if cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet).is_none() { + return Err("The selected spline cannot satisfy curvature continuity."); + } + return Ok(()); + } let validation_target = match driving_param { Some(super::named_parameters::DrivingValue::Named(name)) if !self.named_parameters.contains(name) => @@ -1478,4 +1635,50 @@ mod tests { "the previewed positions should be mutually parallel: dir_a={dir_a:?} dir_b={dir_b:?}" ); } + + #[test] + fn smooth_constraint_recomputes_the_spline_after_its_target_moves() { + let mut scene = Scene::new(); + let target = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(5.0, 0.0, 0.0), + ))); + let mut spline = acadrust::entities::Spline::new(); + spline.degree = 3; + spline.control_points = vec![ + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(1.0, 2.0, 0.0), + Vector3::new(3.0, 2.0, 0.0), + Vector3::new(5.0, 3.0, 0.0), + ]; + spline.knots = cadkernel::space::clamped_uniform_knots(3, 4); + let spline = scene.add_entity(EntityType::Spline(spline)); + scene + .sketch_constraint_set_mut(SketchScope::ModelSpace) + .add( + ConstraintKind::Smooth, + vec![SketchRef::point(spline, 0), SketchRef::point(target, 0)], + None, + ); + + if let Some(EntityType::Line(line)) = scene.document.get_entity_mut(target) { + line.start.y = 1.0; + line.end.y = 1.0; + } + scene.bump_entities(&[(target, super::ChangeKind::Modified)]); + + let EntityType::Spline(spline) = scene.document.get_entity(spline).unwrap() else { + panic!("expected spline"); + }; + let curve = crate::entities::spline::nurbs3(spline).unwrap(); + let jet = cadkernel::space::CurveJet::from_nurbs( + &curve, + cadkernel::space::SplineEnd::Start, + ) + .unwrap(); + assert!((jet.point[0] - 0.0).abs() < 1e-9); + assert!((jet.point[1] - 1.0).abs() < 1e-9); + assert!(jet.tangent[1].abs() < 1e-9, "jet={jet:?}"); + assert!(cadkernel::space::Vec3::from(jet.curvature).length() < 5e-3); + } } diff --git a/src/ui/ribbon/mod.rs b/src/ui/ribbon/mod.rs index badc52b84..a5eb2ffe3 100644 --- a/src/ui/ribbon/mod.rs +++ b/src/ui/ribbon/mod.rs @@ -848,6 +848,9 @@ impl Ribbon { RibbonItem::Dropdown { id, items, default, .. } => (*id, items, *default), + RibbonItem::LabeledDropdown { id, items, default, .. } => { + (*id, items, *default) + } RibbonItem::LargeDropdown { id, items, default, .. } => (*id, items, *default), @@ -1356,7 +1359,8 @@ fn render_group<'a>( for item in &group.tools { let is_large = match item { RibbonItem::LargeTool(_) | RibbonItem::LargeDropdown { .. } => !compact, - RibbonItem::LayerComboGroup { .. } + RibbonItem::ToolGrid { .. } + | RibbonItem::LayerComboGroup { .. } | RibbonItem::PropertiesGroup { .. } | RibbonItem::StyleComboGroup { .. } => true, _ => false, @@ -1400,8 +1404,10 @@ fn render_group<'a>( /// The top-level command id of a ribbon item, if it has one. fn item_id(it: &RibbonItem) -> Option<&'static str> { match it { - RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => Some(t.id), - RibbonItem::Dropdown { id, .. } | RibbonItem::LargeDropdown { id, .. } => Some(*id), + RibbonItem::Tool(t) | RibbonItem::LabeledTool(t) | RibbonItem::LargeTool(t) => Some(t.id), + RibbonItem::Dropdown { id, .. } + | RibbonItem::LabeledDropdown { id, .. } + | RibbonItem::LargeDropdown { id, .. } => Some(*id), RibbonItem::PropertiesGroup { match_prop } => Some(match_prop.id), _ => None, } @@ -1427,13 +1433,17 @@ fn representative<'g>(group: &'g RibbonGroup, last_used: Option<&str>) -> Option /// still get a representative icon. fn first_tool_icon(group: &RibbonGroup) -> Option { group.tools.iter().find_map(|it| match it { - RibbonItem::Tool(t) | RibbonItem::LargeTool(t) => Some(t.icon), - RibbonItem::Dropdown { icon, .. } | RibbonItem::LargeDropdown { icon, .. } => Some(*icon), + RibbonItem::Tool(t) | RibbonItem::LabeledTool(t) | RibbonItem::LargeTool(t) => Some(t.icon), + RibbonItem::Dropdown { icon, .. } + | RibbonItem::LabeledDropdown { icon, .. } + | RibbonItem::LargeDropdown { icon, .. } => Some(*icon), RibbonItem::PropertiesGroup { match_prop } => Some(match_prop.icon), RibbonItem::LayerComboGroup { row2, .. } => row2.first().map(|t| t.icon), RibbonItem::StyleComboGroup { rows, .. } => { rows.first().and_then(|r| r.first()).map(|t| t.icon) - } + }, + RibbonItem::ToolGrid { columns } => columns.first() + .and_then(|column| column.first()).map(|tool| tool.icon), }) } @@ -1611,4 +1621,3 @@ mod tests { assert_eq!(ribbon.open_dropdown, None); } } - diff --git a/src/ui/ribbon/widgets.rs b/src/ui/ribbon/widgets.rs index 6b20ae107..9ee5c2225 100644 --- a/src/ui/ribbon/widgets.rs +++ b/src/ui/ribbon/widgets.rs @@ -59,6 +59,7 @@ const LARGE_LABEL_LINES: f32 = 2.0; const LARGE_LABEL_SIZE: f32 = 10.0; /// Width of a 1-row (small) button. pub(super) const SMALL_W: f32 = ROW_H; +const LABELED_SMALL_W: f32 = ROW_H * 4.0; /// Width of the ▾ strip on a small dropdown. pub(super) const ARROW_W: f32 = ROW_H * 0.4; /// Height of the ▾ strip at the bottom of a large dropdown. @@ -524,6 +525,26 @@ pub(super) fn render_small<'a>( .into() } + RibbonItem::LabeledTool(t) => { + let active = is_active_tool(t.id, active_tool, &state); + let event = t.event.clone(); + let tool_id = t.id.to_string(); + let label = t!(t.label).into_owned(); + let tip_text = format!("{}\n{} {}", label, t!("Command:"), t.id); + let content = row![ + container(make_icon(t.icon, SMALL_ICON)).width(Length::Fixed(SMALL_W)), + text(label).size(10).wrapping(advanced_text::Wrapping::None), + ].spacing(3).align_y(iced::Center); + let btn = button(content) + .on_press(Message::RibbonToolClick { tool_id, event }) + .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) + .width(Length::Fixed(LABELED_SMALL_W + ARROW_W)) + .height(ROW_H) + .padding([3, 4]); + tooltip(btn, make_tip(tip_text), TipPos::Right) + .gap(6.0).delay(Duration::from_millis(400)).style(tip_style).into() + } + RibbonItem::Dropdown { id, icon, @@ -611,6 +632,42 @@ pub(super) fn render_small<'a>( .into() } + RibbonItem::LabeledDropdown { id, label, icon, items, default } => { + let active = active_tool.as_deref() == Some(*id) + || items.iter().any(|(cmd, _, _)| active_tool.as_deref() == Some(*cmd)); + let dd_open = open_dd.as_deref() == Some(*id); + let last = last_cmd.get(id).copied().unwrap_or(*default); + let cur_icon = last_cmd.get(id).copied().and_then(|cmd| { + items.iter().find(|(candidate, _, _)| *candidate == cmd) + .map(|(_, _, item_icon)| *item_icon) + }).or_else(|| items.first().map(|(_, _, item_icon)| *item_icon)).unwrap_or(*icon); + let localized_label = t!(*label).into_owned(); + let face = row![ + container(make_icon(cur_icon, SMALL_ICON)).width(Length::Fixed(SMALL_W)), + text(localized_label.clone()).size(10).wrapping(advanced_text::Wrapping::None), + ].spacing(3).align_y(iced::Center); + let face_btn = button(face) + .on_press(Message::RibbonToolClick { + tool_id: last.to_string(), + event: ModuleEvent::Command(last.to_string()), + }) + .style(move |theme: &Theme, status| tool_btn_style(theme, active, status)) + .width(Length::Fixed(LABELED_SMALL_W)).height(ROW_H).padding([3, 4]); + let arrow = button(container(icons::themed_arrow_down(8.0)) + .width(Fill).height(Fill).align_x(iced::Center).align_y(iced::Center)) + .on_press(Message::ToggleRibbonDropdown(id.to_string())) + .style(move |theme: &Theme, status| tool_btn_style(theme, dd_open, status)) + .width(Length::Fixed(ARROW_W)).height(ROW_H).padding(0); + let face_tip = format!("{}\n{} {}", localized_label, t!("Command:"), last); + let arrow_tip = format!("{} {}", localized_label, t!("options")); + PosReport::new(*id, row![ + tooltip(face_btn, make_tip(face_tip), TipPos::Right) + .gap(6.0).delay(Duration::from_millis(400)).style(tip_style), + tooltip(arrow, make_tip(arrow_tip), TipPos::Right) + .gap(6.0).delay(Duration::from_millis(400)).style(tip_style), + ].spacing(0).height(ROW_H)).into() + } + _ => text("").into(), } } @@ -802,7 +859,7 @@ pub(super) fn render_large<'a>( match item { // A plain Tool renders large too, so a collapsed panel can show its // representative tool as a big icon. - RibbonItem::LargeTool(t) | RibbonItem::Tool(t) => { + RibbonItem::LargeTool(t) | RibbonItem::Tool(t) | RibbonItem::LabeledTool(t) => { let active = is_active_tool(t.id, active_tool, &state); let event = t.event.clone(); let tool_id = t.id.to_string(); @@ -857,6 +914,23 @@ pub(super) fn render_large<'a>( render_large_dropdown(*id, *icon, Some(*label), items, *default, ctx) } + RibbonItem::LabeledDropdown { id, label, icon, items, default } => { + render_large_dropdown(*id, *icon, Some(*label), items, *default, ctx) + } + + RibbonItem::ToolGrid { columns } => columns.iter().fold( + row![].spacing(2).height(Fill).align_y(iced::Top), + |row, tools| { + let column = tools.iter().fold( + column![].spacing(2).width(Length::Fixed(SMALL_W)), + |column, tool| column.push(render_small( + &RibbonItem::Tool(tool.clone()), active_tool, open_dd, last_cmd, state, + )), + ); + row.push(column) + }, + ).into(), + // A plain Dropdown renders large too (used by a collapsed panel whose // representative tool is a dropdown). RibbonItem::Dropdown {