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 @@
+
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 @@
+
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 {