forked from 1b-agent/opencadstudio-upstream
Complete parametric ribbon and constraint tools
This commit is contained in:
1 parent
f3716bb516
commit
7b4cef41f7
49 files changed
+1438
-103
No files matched your search
@@ -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)]
|
||||
|
||||
Reference in new issue
Block a user