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
+291
-5
@@ -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));
|
||||
}
|
||||
}
|
||||
+204
-1
@@ -233,6 +233,40 @@ fn entity_plane_z(entity: &EntityType) -> Option<f64> {
|
||||
{
|
||||
ellipse.center.z
|
||||
}
|
||||
EntityType::Spline(spline) if !spline.flags.closed && !spline.flags.periodic => {
|
||||
let curve = crate::entities::spline::nurbs3(spline)?;
|
||||
let first = *curve.control_points().first()?;
|
||||
if !curve
|
||||
.control_points()
|
||||
.iter()
|
||||
.all(|point| (point[2] - first[2]).abs() <= PLANE_EPS)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
first[2]
|
||||
}
|
||||
EntityType::LwPolyline(polyline) if is_world_z(polyline.normal) => {
|
||||
let points = super::dimension_assoc::source_points(entity);
|
||||
let first = *points.first()?;
|
||||
if !points
|
||||
.iter()
|
||||
.all(|point| (point.z - first.z).abs() <= PLANE_EPS)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
first.z
|
||||
}
|
||||
EntityType::Polyline2D(polyline) if is_world_z(polyline.normal) => {
|
||||
let points = super::dimension_assoc::source_points(entity);
|
||||
let first = *points.first()?;
|
||||
if !points
|
||||
.iter()
|
||||
.all(|point| (point.z - first.z).abs() <= PLANE_EPS)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
first.z
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
z.is_finite().then_some(z)
|
||||
@@ -737,6 +771,9 @@ fn build_constraint(
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
// Smooth edits the endpoint controls through cadkernel's spatial
|
||||
// NURBS operation after the ordinary 2D system has settled.
|
||||
ConstraintKind::Smooth => Vec::new(),
|
||||
// A circle's radius is always normal to its own tangent, so
|
||||
// "line normal to circle/arc" reduces to "line passes through the
|
||||
// circle's center" — the same `PointOnLine` primitive
|
||||
@@ -778,6 +815,85 @@ fn build_constraint(
|
||||
}
|
||||
}
|
||||
|
||||
fn smooth_target_jet(entity: &EntityType, marker: i32) -> Option<cadkernel::space::CurveJet> {
|
||||
let parameter = match marker {
|
||||
0 => 0.0,
|
||||
1 => 1.0,
|
||||
_ => return None,
|
||||
};
|
||||
let curve = crate::entities::curve::entity_curve(entity)?;
|
||||
if curve.is_closed() {
|
||||
return None;
|
||||
}
|
||||
let step = if marker == 0 { 1.0e-4 } else { -1.0e-4 };
|
||||
let point = curve.point_at(parameter);
|
||||
let tangent = curve.tangent_at(parameter);
|
||||
let next = curve.point_at(parameter + step);
|
||||
let third = curve.point_at(parameter + 2.0 * step);
|
||||
Some(cadkernel::space::CurveJet {
|
||||
point,
|
||||
tangent,
|
||||
curvature: cadkernel::space::curve::curvature_through(point, next, third),
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_smooth_constraints(
|
||||
document: &acadrust::CadDocument,
|
||||
set: &SketchConstraintSet,
|
||||
results: &mut Vec<(Handle, EntityType)>,
|
||||
) {
|
||||
for constraint in set
|
||||
.constraints
|
||||
.iter()
|
||||
.filter(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth)
|
||||
{
|
||||
let [source_ref, target_ref] = constraint.refs.as_slice() else {
|
||||
continue;
|
||||
};
|
||||
let source = results
|
||||
.iter()
|
||||
.find(|(handle, _)| *handle == source_ref.entity)
|
||||
.map(|(_, entity)| entity.clone())
|
||||
.or_else(|| document.get_entity(source_ref.entity).cloned());
|
||||
let target = results
|
||||
.iter()
|
||||
.find(|(handle, _)| *handle == target_ref.entity)
|
||||
.map(|(_, entity)| entity.clone())
|
||||
.or_else(|| document.get_entity(target_ref.entity).cloned());
|
||||
let (Some(EntityType::Spline(mut spline)), Some(target)) = (source, target) else {
|
||||
continue;
|
||||
};
|
||||
let endpoint = match source_ref.marker {
|
||||
Some(0) => cadkernel::space::SplineEnd::Start,
|
||||
Some(1) => cadkernel::space::SplineEnd::End,
|
||||
_ => continue,
|
||||
};
|
||||
let Some(target_marker) = target_ref.marker else {
|
||||
continue;
|
||||
};
|
||||
let Some(target_jet) = smooth_target_jet(&target, target_marker) else {
|
||||
continue;
|
||||
};
|
||||
let Some(curve) = crate::entities::spline::nurbs3(&spline) else {
|
||||
continue;
|
||||
};
|
||||
let Some(smoothed) = cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
crate::entities::spline::replace_with_nurbs(&mut spline, &smoothed);
|
||||
let updated = EntityType::Spline(spline);
|
||||
if let Some((_, entity)) = results
|
||||
.iter_mut()
|
||||
.find(|(handle, _)| *handle == source_ref.entity)
|
||||
{
|
||||
*entity = updated;
|
||||
} else {
|
||||
results.push((source_ref.entity, updated));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const MOVE_EPS: f64 = 1e-9;
|
||||
type SolveResult = (
|
||||
Vec<(Handle, EntityType)>,
|
||||
@@ -823,7 +939,11 @@ fn solve_scope(
|
||||
}
|
||||
}
|
||||
|
||||
if cache.is_empty() {
|
||||
let has_smooth = set
|
||||
.constraints
|
||||
.iter()
|
||||
.any(|constraint| constraint.enabled && constraint.kind == ConstraintKind::Smooth);
|
||||
if cache.is_empty() && !has_smooth {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1065,6 +1185,7 @@ fn solve_scope(
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
apply_smooth_constraints(document, set, &mut results);
|
||||
Some((results, dof, conflicts))
|
||||
}
|
||||
|
||||
@@ -1082,6 +1203,42 @@ impl Scene {
|
||||
"Constraint references must be supported entities on the same world-XY plane.",
|
||||
);
|
||||
}
|
||||
if kind == ConstraintKind::Smooth {
|
||||
let [source_ref, target_ref] = refs else {
|
||||
return Err("Smooth requires one spline endpoint and one target endpoint.");
|
||||
};
|
||||
if source_ref.entity == target_ref.entity {
|
||||
return Err("Smooth requires two different entities.");
|
||||
}
|
||||
let Some(EntityType::Spline(spline)) = self.document.get_entity(source_ref.entity)
|
||||
else {
|
||||
return Err("Smooth requires an open spline as its first reference.");
|
||||
};
|
||||
if spline.flags.closed || spline.flags.periodic {
|
||||
return Err("Smooth requires an open spline.");
|
||||
}
|
||||
let endpoint = match source_ref.marker {
|
||||
Some(0) => cadkernel::space::SplineEnd::Start,
|
||||
Some(1) => cadkernel::space::SplineEnd::End,
|
||||
_ => return Err("Smooth requires a spline endpoint."),
|
||||
};
|
||||
let Some(target_marker) = target_ref.marker else {
|
||||
return Err("Smooth requires a target endpoint.");
|
||||
};
|
||||
let Some(target) = self.document.get_entity(target_ref.entity) else {
|
||||
return Err("Smooth target does not exist.");
|
||||
};
|
||||
let Some(target_jet) = smooth_target_jet(target, target_marker) else {
|
||||
return Err("The selected target does not support endpoint smoothing.");
|
||||
};
|
||||
let Some(curve) = crate::entities::spline::nurbs3(spline) else {
|
||||
return Err("The selected spline cannot be evaluated.");
|
||||
};
|
||||
if cadkernel::space::smooth_nurbs_endpoint(&curve, endpoint, target_jet).is_none() {
|
||||
return Err("The selected spline cannot satisfy curvature continuity.");
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
let validation_target = match driving_param {
|
||||
Some(super::named_parameters::DrivingValue::Named(name))
|
||||
if !self.named_parameters.contains(name) =>
|
||||
@@ -1478,4 +1635,50 @@ mod tests {
|
||||
"the previewed positions should be mutually parallel: dir_a={dir_a:?} dir_b={dir_b:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn smooth_constraint_recomputes_the_spline_after_its_target_moves() {
|
||||
let mut scene = Scene::new();
|
||||
let target = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(5.0, 0.0, 0.0),
|
||||
)));
|
||||
let mut spline = acadrust::entities::Spline::new();
|
||||
spline.degree = 3;
|
||||
spline.control_points = vec![
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 2.0, 0.0),
|
||||
Vector3::new(3.0, 2.0, 0.0),
|
||||
Vector3::new(5.0, 3.0, 0.0),
|
||||
];
|
||||
spline.knots = cadkernel::space::clamped_uniform_knots(3, 4);
|
||||
let spline = scene.add_entity(EntityType::Spline(spline));
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Smooth,
|
||||
vec![SketchRef::point(spline, 0), SketchRef::point(target, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
if let Some(EntityType::Line(line)) = scene.document.get_entity_mut(target) {
|
||||
line.start.y = 1.0;
|
||||
line.end.y = 1.0;
|
||||
}
|
||||
scene.bump_entities(&[(target, super::ChangeKind::Modified)]);
|
||||
|
||||
let EntityType::Spline(spline) = scene.document.get_entity(spline).unwrap() else {
|
||||
panic!("expected spline");
|
||||
};
|
||||
let curve = crate::entities::spline::nurbs3(spline).unwrap();
|
||||
let jet = cadkernel::space::CurveJet::from_nurbs(
|
||||
&curve,
|
||||
cadkernel::space::SplineEnd::Start,
|
||||
)
|
||||
.unwrap();
|
||||
assert!((jet.point[0] - 0.0).abs() < 1e-9);
|
||||
assert!((jet.point[1] - 1.0).abs() < 1e-9);
|
||||
assert!(jet.tangent[1].abs() < 1e-9, "jet={jet:?}");
|
||||
assert!(cadkernel::space::Vec3::from(jet.curvature).length() < 5e-3);
|
||||
}
|
||||
}
|
||||
+16
-7
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user