Complete parametric ribbon and constraint tools

This commit is contained in:
Hakan Seven committed 2026-09-14 01:05:39 +03:00
1 parent f3716bb516
commit 7b4cef41f7
49 files changed
+1438 -103

No files matched your search

Generated
+2 -2
View File
@@ -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",
]
+2 -2
View File
@@ -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"] }
+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M4 17L11 5l9 14" stroke="#B4B6B9" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path d="M5 11h5M14 15h6" stroke="#6DB7ED" stroke-width="2" stroke-linecap="round"/>
<circle cx="5" cy="11" r="1.7" fill="#6DB7ED"/><circle cx="20" cy="15" r="1.7" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 399 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M5 8h11l-3-3m3 3l-3 3M19 16H8l3-3m-3 3l3 3" stroke="#6DB7ED" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<path d="M4 4v16M20 4v16" stroke="#B4B6B9" stroke-width="1.6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 330 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M7 7h10l-1 13H8L7 7zM5 7h14M9 7V4h6v3" stroke="#B4B6B9" stroke-width="1.8" stroke-linejoin="round" fill="none"/>
<path d="M9.5 10.5l5 5m0-5l-5 5" stroke="#6DB7ED" stroke-width="1.9" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 311 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M4 5v14M20 5v14" stroke="#B4B6B9" stroke-width="1.7" stroke-linecap="round"/>
<path d="M5 12h14m-14 0l3-3m-3 3l3 3m11-3l-3-3m3 3l-3 3" stroke="#6DB7ED" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<text x="10" y="8" font-size="5" fill="#6DB7ED" font-family="sans-serif">X</text>
</svg>

After

Width:  |  Height:  |  Size: 408 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M5 4h14M5 20h14" stroke="#B4B6B9" stroke-width="1.7" stroke-linecap="round"/>
<path d="M12 5v14m0-14l-3 3m3-3l3 3m-3 11l-3-3m3 3l3-3" stroke="#6DB7ED" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/>
<text x="15" y="14" font-size="5" fill="#6DB7ED" font-family="sans-serif">Y</text>
</svg>

After

Width:  |  Height:  |  Size: 408 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M2.5 12s3.5-5 9.5-5 9.5 5 9.5 5-3.5 5-9.5 5-9.5-5-9.5-5z" stroke="#B4B6B9" stroke-width="1.8" fill="none"/>
<path d="M4 4l16 16" stroke="#6DB7ED" stroke-width="2.2" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 294 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M5 5h14M5 12h14M5 19h14" stroke="#B4B6B9" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="9" cy="5" r="2.1" fill="#6DB7ED"/><circle cx="15" cy="12" r="2.1" fill="#6DB7ED"/><circle cx="11" cy="19" r="2.1" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 333 B

+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M2.5 12s3.5-5 9.5-5 9.5 5 9.5 5-3.5 5-9.5 5-9.5-5-9.5-5z" stroke="#B4B6B9" stroke-width="1.8" fill="none"/>
<circle cx="12" cy="12" r="2.7" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 261 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M2.5 10s3.5-4.5 9.5-4.5 9.5 4.5 9.5 4.5-3.5 4.5-9.5 4.5S2.5 10 2.5 10z" stroke="#B4B6B9" stroke-width="1.8" fill="none"/>
<circle cx="12" cy="10" r="2.4" fill="#6DB7ED"/>
<path d="M7 18h10M12 15v6" stroke="#6DB7ED" stroke-width="1.8" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

+5
View File
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="64" height="64">
<path d="M3 18C8 18 7 6 13 6c4 0 4 6 8 6" stroke="#B4B6B9" stroke-width="2.1" stroke-linecap="round" fill="none"/>
<path d="M13 6c4 0 4 6 8 6" stroke="#6DB7ED" stroke-width="2.1" stroke-linecap="round" fill="none"/>
<circle cx="13" cy="6" r="1.6" fill="#6DB7ED"/>
</svg>

After

Width:  |  Height:  |  Size: 361 B

+5 -5
View File
@@ -1,7 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" role="img" aria-label="Dim Aligned">
<line x1="5" y1="6" x2="5" y2="18" stroke="#B4B6B9" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="19" y1="6" x2="19" y2="18" stroke="#B4B6B9" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="7" y1="11" x2="17" y2="11" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<polyline points="9,9 7,11 9,13" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<polyline points="15,9 17,11 15,13" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<line x1="5" y1="17" x2="9" y2="21" stroke="#B4B6B9" stroke-width="1.625" stroke-linecap="round"/>
<line x1="15" y1="3" x2="19" y2="7" stroke="#B4B6B9" stroke-width="1.625" stroke-linecap="round"/>
<line x1="7" y1="18" x2="17" y2="6" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round"/>
<polyline points="7.5,14.5 7,18 10.5,17.2" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
<polyline points="13.5,6.8 17,6 16.5,9.5" stroke="#6DB7ED" stroke-width="1.625" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

Before

Width:  |  Height:  |  Size: 787 B

After

Width:  |  Height:  |  Size: 694 B

+12
View File
@@ -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<Vec<ToolDef>> },
/// 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)
+110 -2
View File
@@ -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<Vec<OwnedToolDef>>,
},
LayerComboGroup {
row2: Vec<OwnedToolDef>,
row3: Vec<OwnedToolDef>,
@@ -79,11 +90,20 @@ fn push_tool_command(tool: &OwnedToolDef, out: &mut Vec<String>) {
fn collect_item_command_ids(item: &OwnedRibbonItem, out: &mut Vec<String>) {
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<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,
@@ -193,6 +214,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.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<RibbonItem> 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);
}
}
+9
View File
@@ -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 = تعذر حفظ الرسم
+9
View File
@@ -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 = Чертежът не може да бъде запазен
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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 = Αδυναμία αποθήκευσης σχεδίου
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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 = ड्रॉइंग सहेजी नहीं जा सकी
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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 = 図面を保存できません
+9
View File
@@ -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 = 도면을 저장할 수 없습니다
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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
+9
View File
@@ -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 = Не удалось сохранить чертёж
+9
View File
@@ -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
+9
View File
@@ -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 = 无法保存图形
+9
View File
@@ -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 = 無法儲存圖形
+291 -5
View File
@@ -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));
+1
View File
@@ -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(
+37
View File
@@ -47,6 +47,43 @@ pub(crate) fn fit_nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
tangent(spline.end_tangent), parameterization)
}
pub(crate) fn nurbs3(spline: &Spline) -> Option<NurbsCurve3> {
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
+8
View File
@@ -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")),
+98 -65
View File
@@ -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<Vec<RibbonGroup>> = 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::<Vec<_>>(),
["Geometry", "Dimension", "Manage"]
["Geometric", "Dimensional", "Manage"]
);
assert_eq!(
groups[0].tools.iter().map(item_id).collect::<Vec<_>>(),
[
"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::<Vec<_>>(),
["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::<Vec<_>>(), ["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::<Vec<_>>(),
[
"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::<Vec<_>>(), [
"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::<Vec<_>>(), [
"DCANGULAR", "DCDIAMETER", "DCRADIUS", "DCCONVERT",
]);
}
}
+72 -13
View File
@@ -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> {
Self::with_mode(scene, handle, "DCONSTRAINT", DistanceMode::Auto)
}
pub fn with_mode(
scene: &Scene,
handle: Handle,
command_name: &'static str,
requested_mode: DistanceMode,
) -> Option<Self> {
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> {
Self::with_name(scene, fixed, moving, "ACONSTRAINT")
}
pub fn with_name(
scene: &Scene,
fixed: Handle,
moving: Handle,
command_name: &'static str,
) -> Option<Self> {
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)]
+59
View File
@@ -275,6 +275,12 @@ pub(crate) fn source_points(entity: &EntityType) -> Vec<Vector3> {
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::sketch_constraints::SketchRef>,
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<Handle> = 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,
+1
View File
@@ -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.
+6
View File
@@ -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<sketch_constraints::SketchConstraintSet>,
/// 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()),
+205
View File
@@ -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<SketchRef>)> {
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<Vec<SketchRef>> {
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));
}
}
+204 -1
View File
@@ -233,6 +233,40 @@ fn entity_plane_z(entity: &EntityType) -> Option<f64> {
{
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<cadkernel::space::CurveJet> {
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);
}
}
+16 -7
View File
@@ -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<IconKind> {
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);
}
}
+75 -1
View File
@@ -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 {