Complete parametric ribbon and constraint tools

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

No files matched your search

+98 -65
View File
@@ -16,7 +16,10 @@ pub use tools::{
colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric,
tangent, vertical,
};
pub use value::{angle_tool, distance_tool, AngleConstraintCommand, DistanceConstraintCommand};
pub use value::{
angle_tool, dimensional_tools, distance_tool, AngleConstraintCommand,
DistanceConstraintCommand, DistanceMode,
};
use crate::modules::{CadModule, IconKind, ModuleEvent, RibbonGroup, RibbonItem, ToolDef};
@@ -34,68 +37,107 @@ impl CadModule for ParametricModule {
fn ribbon_groups(&self) -> &[RibbonGroup] {
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
GROUPS.get_or_init(|| {
let extra_constraints = [
normal::tool(),
center_point_tool::tool(),
midpoint_tool::tool(),
point_on_curve_tool::tool(),
equal_distance_tool::tool(),
];
let command = |id: &'static str, label: &'static str, icon: &'static [u8]| ToolDef {
id, label, icon: IconKind::Svg(icon), event: ModuleEvent::Command(id.to_string()),
};
vec![
RibbonGroup {
title: "Geometry",
title: "Geometric",
tools: vec![
coincident_tool::tool().into(),
parallel::tool().into(),
tangent::tool().into(),
colinear::tool().into(),
perpendicular::tool().into(),
RibbonItem::Dropdown {
id: "PARAMETRIC_MORE",
icon: extra_constraints[0].icon,
items: extra_constraints
.iter()
.map(|tool| (tool.id, tool.label, tool.icon))
.collect(),
default: "NRCONSTRAINT",
RibbonItem::LargeTool(command(
"AUTOCONSTRAIN", "Auto Constrain",
include_bytes!("../../../assets/icons/constrain/auto.svg"),
)),
RibbonItem::ToolGrid { columns: vec![
vec![coincident_tool::tool(), parallel::tool(), tangent::tool()],
vec![
colinear::tool(), perpendicular::tool(),
command("SMOOTHCONSTRAINT", "Smooth", include_bytes!("../../../assets/icons/constrain/smooth.svg")),
],
vec![concentric::tool(), horizontal::tool(), symmetric::tool()],
vec![fixed::tool(), vertical::tool(), equal::tool()],
] },
RibbonItem::LabeledDropdown {
id: "GCVISIBILITY", label: "Show/Hide",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")),
items: vec![
("GCSHOW", "Show", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg"))),
("GCHIDE", "Hide", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/hide_all.svg"))),
("GCRESET", "Reset", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg"))),
], default: "GCSHOW",
},
concentric::tool().into(),
horizontal::tool().into(),
symmetric::tool().into(),
fixed::tool().into(),
vertical::tool().into(),
equal::tool().into(),
RibbonItem::LabeledTool(command(
"GCSHOWALL", "Show All", include_bytes!("../../../assets/icons/constrain/show_all.svg"),
)),
RibbonItem::LabeledTool(command(
"GCHIDEALL", "Hide All", include_bytes!("../../../assets/icons/constrain/hide_all.svg"),
)),
],
},
RibbonGroup {
title: "Dimension",
title: "Dimensional",
tools: vec![
RibbonItem::LargeTool(distance_tool::tool()),
RibbonItem::LargeTool(angle_tool::tool()),
RibbonItem::LargeDropdown {
id: "DC_LINEAR_MENU", label: "Linear", icon: dimensional_tools::linear().icon,
items: [dimensional_tools::linear(), dimensional_tools::horizontal(), dimensional_tools::vertical()]
.iter().map(|tool| (tool.id, tool.label, tool.icon)).collect(),
default: "DCLINEAR",
},
RibbonItem::LargeTool(dimensional_tools::aligned()),
RibbonItem::ToolGrid { columns: vec![
vec![dimensional_tools::angular(), dimensional_tools::diameter()],
vec![dimensional_tools::radius(), dimensional_tools::convert()],
] },
RibbonItem::LabeledDropdown {
id: "DCVISIBILITY", label: "Show/Hide",
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg")),
items: vec![
("DCSHOW", "Show", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/show.svg"))),
("DCHIDE", "Hide", IconKind::Svg(include_bytes!("../../../assets/icons/constrain/hide_all.svg"))),
], default: "DCSHOW",
},
RibbonItem::LabeledTool(command(
"DCSHOWALL", "Show All", include_bytes!("../../../assets/icons/constrain/show_all.svg"),
)),
RibbonItem::LabeledTool(command(
"DCHIDEALL", "Hide All", include_bytes!("../../../assets/icons/constrain/hide_all.svg"),
)),
],
},
RibbonGroup {
title: "Manage",
tools: vec![RibbonItem::LargeTool(ToolDef {
id: "PARAMETERS",
label: "Parameters",
icon: IconKind::Glyph("ƒ"),
event: ModuleEvent::Command("PARAMETERS".to_string()),
})],
tools: vec![
RibbonItem::LargeTool(command(
"DELCONSTRAINT", "Delete Constraints", include_bytes!("../../../assets/icons/constrain/delete.svg"),
)),
RibbonItem::LargeTool(command(
"PARAMETERS", "Parameters Manager", include_bytes!("../../../assets/icons/constrain/parameters.svg"),
)),
],
},
]
})
}
}
inventory::submit!(crate::command::CommandRegistration {
names: &[
"AUTOCONSTRAIN", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET",
"GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL",
"DCCONVERT", "DELCONSTRAINT",
]
});
#[cfg(test)]
mod tests {
use super::*;
fn item_id(item: &RibbonItem) -> &'static str {
match item {
RibbonItem::Tool(tool) | RibbonItem::LargeTool(tool) => tool.id,
RibbonItem::Dropdown { id, .. } | RibbonItem::LargeDropdown { id, .. } => id,
RibbonItem::Tool(tool) | RibbonItem::LabeledTool(tool) | RibbonItem::LargeTool(tool) => tool.id,
RibbonItem::Dropdown { id, .. } | RibbonItem::LabeledDropdown { id, .. }
| RibbonItem::LargeDropdown { id, .. } => id,
RibbonItem::ToolGrid { .. } => "GRID",
_ => panic!("unexpected composite ribbon item"),
}
}
@@ -106,43 +148,34 @@ mod tests {
assert_eq!(
groups.iter().map(|group| group.title).collect::<Vec<_>>(),
["Geometry", "Dimension", "Manage"]
["Geometric", "Dimensional", "Manage"]
);
assert_eq!(
groups[0].tools.iter().map(item_id).collect::<Vec<_>>(),
[
"CCONSTRAINT",
"PCONSTRAINT",
"TCONSTRAINT",
"LCONSTRAINT",
"QCONSTRAINT",
"PARAMETRIC_MORE",
"NCONSTRAINT",
"HCONSTRAINT",
"SYCONSTRAINT",
"FXCONSTRAINT",
"VCONSTRAINT",
"ECONSTRAINT",
]
["AUTOCONSTRAIN", "GRID", "GCVISIBILITY", "GCSHOWALL", "GCHIDEALL"]
);
assert_eq!(
groups[1].tools.iter().map(item_id).collect::<Vec<_>>(),
["DCONSTRAINT", "ACONSTRAINT"]
["DC_LINEAR_MENU", "DCALIGNED", "GRID", "DCVISIBILITY", "DCSHOWALL", "DCHIDEALL"]
);
assert_eq!(item_id(&groups[2].tools[0]), "PARAMETERS");
assert_eq!(groups[2].tools.iter().map(item_id).collect::<Vec<_>>(), ["DELCONSTRAINT", "PARAMETERS"]);
let RibbonItem::Dropdown { items, .. } = &groups[0].tools[5] else {
panic!("additional geometric constraints must stay in a dropdown");
let RibbonItem::LargeDropdown { items, default, .. } = &groups[1].tools[0] else {
panic!("linear dimensional constraint must be a split dropdown");
};
assert_eq!(*default, "DCLINEAR");
assert_eq!(
items.iter().map(|(id, _, _)| *id).collect::<Vec<_>>(),
[
"NRCONSTRAINT",
"CPCONSTRAINT",
"MPCONSTRAINT",
"OCCONSTRAINT",
"EDCONSTRAINT",
]
["DCLINEAR", "DCHORIZONTAL", "DCVERTICAL"]
);
let RibbonItem::ToolGrid { columns } = &groups[0].tools[1] else { panic!("geometry grid") };
assert_eq!(columns.iter().flatten().map(|tool| tool.id).collect::<Vec<_>>(), [
"CCONSTRAINT", "PCONSTRAINT", "TCONSTRAINT", "LCONSTRAINT", "QCONSTRAINT", "SMOOTHCONSTRAINT",
"NCONSTRAINT", "HCONSTRAINT", "SYCONSTRAINT", "FXCONSTRAINT", "VCONSTRAINT", "ECONSTRAINT",
]);
let RibbonItem::ToolGrid { columns } = &groups[1].tools[2] else { panic!("dimension grid") };
assert_eq!(columns.iter().flatten().map(|tool| tool.id).collect::<Vec<_>>(), [
"DCANGULAR", "DCDIAMETER", "DCRADIUS", "DCCONVERT",
]);
}
}
+72 -13
View File
@@ -60,6 +60,23 @@ pub mod distance_tool {
}
}
pub mod dimensional_tools {
use super::*;
fn command(id: &'static str, label: &'static str, icon: &'static [u8]) -> ToolDef {
ToolDef { id, label, icon: IconKind::Svg(icon), event: ModuleEvent::Command(id.to_string()) }
}
pub fn linear() -> ToolDef { command("DCLINEAR", "Linear", include_bytes!("../../../assets/icons/dim_linear.svg")) }
pub fn horizontal() -> ToolDef { command("DCHORIZONTAL", "Horizontal", include_bytes!("../../../assets/icons/constrain/distance_x.svg")) }
pub fn vertical() -> ToolDef { command("DCVERTICAL", "Vertical", include_bytes!("../../../assets/icons/constrain/distance_y.svg")) }
pub fn aligned() -> ToolDef { command("DCALIGNED", "Aligned", include_bytes!("../../../assets/icons/dim_aligned.svg")) }
pub fn angular() -> ToolDef { command("DCANGULAR", "Angular", include_bytes!("../../../assets/icons/dim_angular.svg")) }
pub fn radius() -> ToolDef { command("DCRADIUS", "Radius", include_bytes!("../../../assets/icons/dim_radius.svg")) }
pub fn diameter() -> ToolDef { command("DCDIAMETER", "Diameter", include_bytes!("../../../assets/icons/dim_diameter.svg")) }
pub fn convert() -> ToolDef { command("DCCONVERT", "Convert", include_bytes!("../../../assets/icons/constrain/convert.svg")) }
}
pub mod angle_tool {
use super::*;
pub fn tool() -> ToolDef {
@@ -76,10 +93,14 @@ pub mod angle_tool {
/// Which flavor of dimensional constraint the typed value drives.
#[derive(Clone, Copy, PartialEq, Eq)]
enum DistanceMode {
pub enum DistanceMode {
/// Line length (two-point distance) or a circle/arc's radius —
/// whichever `is_circle` selects.
Auto,
/// Chooses the dominant world-axis component.
Linear,
Aligned,
Radius,
X,
Y,
Diameter,
@@ -92,6 +113,7 @@ enum DistanceMode {
/// its two endpoints, or a circle/arc's radius (or diameter, or — for an
/// arc specifically — its swept arc length), to a typed target value.
pub struct DistanceConstraintCommand {
command_name: &'static str,
handle: Handle,
/// Whether `handle` is a circle/arc (Radius/Diameter) or a line
/// (Distance/DistanceX/DistanceY) — decided once at construction from
@@ -115,6 +137,15 @@ pub struct DistanceConstraintCommand {
impl DistanceConstraintCommand {
/// `None` if `handle` isn't a Line, Circle, or Arc.
pub fn new(scene: &Scene, handle: Handle) -> Option<Self> {
Self::with_mode(scene, handle, "DCONSTRAINT", DistanceMode::Auto)
}
pub fn with_mode(
scene: &Scene,
handle: Handle,
command_name: &'static str,
requested_mode: DistanceMode,
) -> Option<Self> {
let entity = scene.document.get_entity(handle)?;
let (is_circle, is_arc, default_value, default_x, default_y, default_arc_length) =
match entity {
@@ -132,11 +163,23 @@ impl DistanceConstraintCommand {
.iter()
.map(|p| p.name.clone())
.collect();
let mode = match requested_mode {
DistanceMode::Linear if !is_circle => {
if default_x.abs() >= default_y.abs() { DistanceMode::X } else { DistanceMode::Y }
}
DistanceMode::Aligned if !is_circle => DistanceMode::Aligned,
DistanceMode::Radius if is_circle => DistanceMode::Radius,
DistanceMode::Diameter if is_circle => DistanceMode::Diameter,
DistanceMode::X | DistanceMode::Y if !is_circle => requested_mode,
DistanceMode::Auto => DistanceMode::Auto,
_ => return None,
};
Some(Self {
command_name,
handle,
is_circle,
is_arc,
mode: DistanceMode::Auto,
mode,
default_value,
default_x,
default_y,
@@ -163,7 +206,7 @@ impl DistanceConstraintCommand {
vec![SketchRef::whole(self.handle)],
"Arc length constraint",
),
(true, _) => (
(true, DistanceMode::Auto | DistanceMode::Radius) => (
ConstraintKind::Radius,
vec![SketchRef::whole(self.handle)],
"Radius constraint",
@@ -184,7 +227,7 @@ impl DistanceConstraintCommand {
],
"DistanceY constraint",
),
(false, _) => (
(false, DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear) => (
ConstraintKind::Distance,
vec![
SketchRef::point(self.handle, 0),
@@ -192,6 +235,7 @@ impl DistanceConstraintCommand {
],
"Distance constraint",
),
_ => return None,
};
Some(CmdResult::AddSketchConstraint {
kind,
@@ -204,7 +248,7 @@ impl DistanceConstraintCommand {
impl CadCommand for DistanceConstraintCommand {
fn name(&self) -> &'static str {
"DCONSTRAINT"
self.command_name
}
fn prompt(&self) -> String {
@@ -219,20 +263,21 @@ impl CadCommand for DistanceConstraintCommand {
self.default_arc_length
)
}
(true, _) if self.is_arc => format!(
(true, DistanceMode::Auto | DistanceMode::Radius) if self.is_arc => format!(
"Specify distance <{:.4}> or [Diameter/Arclength]: ",
self.default_value
),
(true, _) => format!(
(true, DistanceMode::Auto | DistanceMode::Radius) => format!(
"Specify distance <{:.4}> or [Diameter]: ",
self.default_value
),
(false, DistanceMode::X) => format!("Specify X distance <{:.4}>: ", self.default_x),
(false, DistanceMode::Y) => format!("Specify Y distance <{:.4}>: ", self.default_y),
(false, _) => format!(
(false, DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear) => format!(
"Specify distance <{:.4}> or [Xdistance/Ydistance]: ",
self.default_value
),
_ => "Unsupported distance constraint mode.".to_string(),
}
}
@@ -247,10 +292,10 @@ impl CadCommand for DistanceConstraintCommand {
fn on_enter(&mut self) -> CmdResult {
let default = match self.mode {
DistanceMode::Diameter => self.default_value * 2.0,
DistanceMode::ArcLength if self.is_arc => self.default_arc_length,
DistanceMode::ArcLength => self.default_arc_length,
DistanceMode::X => self.default_x,
DistanceMode::Y => self.default_y,
_ => self.default_value,
DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear | DistanceMode::Radius => self.default_value,
};
self.build(DrivingValue::Literal(default))
.unwrap_or(CmdResult::Cancel)
@@ -274,7 +319,7 @@ impl CadCommand for DistanceConstraintCommand {
return Some(CmdResult::NeedPoint);
}
(true, "R" | "RADIUS") => {
self.mode = DistanceMode::Auto;
self.mode = DistanceMode::Radius;
return Some(CmdResult::NeedPoint);
}
(false, "X" | "XDISTANCE") => {
@@ -298,6 +343,7 @@ impl CadCommand for DistanceConstraintCommand {
/// Constrains the angle (in degrees) from `fixed`'s direction to `moving`'s.
pub struct AngleConstraintCommand {
command_name: &'static str,
fixed_handle: Handle,
moving_handle: Handle,
default_value: f64,
@@ -309,6 +355,15 @@ pub struct AngleConstraintCommand {
impl AngleConstraintCommand {
/// `None` unless both `fixed` and `moving` are lines.
pub fn new(scene: &Scene, fixed: Handle, moving: Handle) -> Option<Self> {
Self::with_name(scene, fixed, moving, "ACONSTRAINT")
}
pub fn with_name(
scene: &Scene,
fixed: Handle,
moving: Handle,
command_name: &'static str,
) -> Option<Self> {
let fixed_entity = scene.document.get_entity(fixed)?;
let moving_entity = scene.document.get_entity(moving)?;
let (EntityType::Line(f), EntityType::Line(m)) = (fixed_entity, moving_entity) else {
@@ -323,6 +378,7 @@ impl AngleConstraintCommand {
.map(|p| p.name.clone())
.collect();
Some(Self {
command_name,
fixed_handle: fixed,
moving_handle: moving,
default_value,
@@ -348,7 +404,7 @@ impl AngleConstraintCommand {
impl CadCommand for AngleConstraintCommand {
fn name(&self) -> &'static str {
"ACONSTRAINT"
self.command_name
}
fn prompt(&self) -> String {
@@ -380,7 +436,10 @@ impl CadCommand for AngleConstraintCommand {
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration {
names: &["DCONSTRAINT", "ACONSTRAINT"]
names: &[
"DCONSTRAINT", "ACONSTRAINT", "DCLINEAR", "DCHORIZONTAL", "DCVERTICAL",
"DCALIGNED", "DCANGULAR", "DCRADIUS", "DCDIAMETER",
]
});
#[cfg(test)]