Complete parametric ribbon and constraint tools
No files matched your search
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 = تعذر حفظ الرسم
|
||||
|
||||
@@ -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 = Чертежът не може да бъде запазен
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Αδυναμία αποθήκευσης σχεδίου
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = ड्रॉइंग सहेजी नहीं जा सकी
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 図面を保存できません
|
||||
|
||||
@@ -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 = 도면을 저장할 수 없습니다
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = Не удалось сохранить чертёж
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = 无法保存图形
|
||||
|
||||
@@ -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 = 無法儲存圖形
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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",
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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)]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||