forked from 1b-agent/opencadstudio-upstream
Merge remote-tracking branch 'origin/main' into feat/viewport-dimensions
# Conflicts: # src/app/command_driver.rs # src/scene/mod.rs
This commit is contained in:
commit
369e0c9fc5
89 files changed
+27311
-11037
No files matched your search
@@ -42,7 +42,7 @@ impl CadModule for AnnotateModule {
|
||||
}
|
||||
|
||||
fn ribbon_groups(&self) -> &[RibbonGroup] {
|
||||
use crate::modules::draw::draw::{revcloud, wipeout};
|
||||
use crate::modules::draw::draw::{centerline, dimcenter, revcloud, wipeout};
|
||||
|
||||
static GROUPS: std::sync::OnceLock<Vec<RibbonGroup>> = std::sync::OnceLock::new();
|
||||
GROUPS.get_or_init(|| {
|
||||
@@ -148,6 +148,44 @@ impl CadModule for AnnotateModule {
|
||||
},
|
||||
],
|
||||
},
|
||||
// ── Centerlines ───────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
title: "Centerlines",
|
||||
tools: vec![
|
||||
RibbonItem::LargeTool(dimcenter::tool()),
|
||||
RibbonItem::LargeTool(centerline::tool()),
|
||||
RibbonItem::Dropdown {
|
||||
id: "ANNOTATE_CENTER_ASSOCIATIVITY",
|
||||
icon: crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/line.svg"
|
||||
)),
|
||||
items: vec![
|
||||
(
|
||||
"CENTERREASSOCIATE",
|
||||
"Reassociate Center Object",
|
||||
crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/line.svg"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"CENTERDISASSOCIATE",
|
||||
"Disassociate Center Object",
|
||||
crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/line.svg"
|
||||
)),
|
||||
),
|
||||
(
|
||||
"CENTERRESET",
|
||||
"Reset Center Object",
|
||||
crate::modules::IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/line.svg"
|
||||
)),
|
||||
),
|
||||
],
|
||||
default: "CENTERREASSOCIATE",
|
||||
},
|
||||
],
|
||||
},
|
||||
// ── Leaders ──────────────────────────────────────────────────
|
||||
RibbonGroup {
|
||||
title: "Leaders",
|
||||
|
||||
@@ -304,6 +304,14 @@ impl AlignCommand {
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ALIGN"] }); // AlignCommand
|
||||
// ALIGNLEFT/ALIGNHCENTER/ALIGNRIGHT/ALIGNTOP/ALIGNVCENTER/ALIGNBOTTOM: one-shot
|
||||
// bounding-box alignment, dispatched directly (no CadCommand of their own) by
|
||||
// `OpenCADStudio::align_selected_bounds` in `src/app/commands/inquiry.rs`.
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &[
|
||||
"ALIGNLEFT", "ALIGNHCENTER", "ALIGNRIGHT", "ALIGNTOP", "ALIGNVCENTER", "ALIGNBOTTOM",
|
||||
],
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
@@ -927,6 +927,19 @@ fn explode_dimension(dim: &Dimension, doc: &CadDocument) -> Vec<EntityType> {
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
if let Some((local, normal)) = crate::entities::dimension::linear_dimension_in_ocs(dim) {
|
||||
let (x_axis, y_axis) =
|
||||
crate::scene::view::transform::ocs_axes((normal.x, normal.y, normal.z));
|
||||
let plane = WorkingPlane::new(
|
||||
DVec3::ZERO,
|
||||
DVec3::new(x_axis.0, x_axis.1, x_axis.2),
|
||||
DVec3::new(y_axis.0, y_axis.1, y_axis.2),
|
||||
);
|
||||
return explode_dimension(&local, doc)
|
||||
.into_iter()
|
||||
.map(|entity| plane.place_entity(entity))
|
||||
.collect();
|
||||
}
|
||||
|
||||
let base = dim.base();
|
||||
let met = dim_metrics(dim, doc);
|
||||
@@ -1659,6 +1672,58 @@ mod tests {
|
||||
assert_eq!(solids, 2, "expected 2 filled (SOLID) arrowheads, got {solids}");
|
||||
}
|
||||
|
||||
/// The line segments EXPLODE turns the dimension into.
|
||||
fn exploded_segments(dim: &Dimension, doc: &CadDocument) -> Vec<(DVec3, DVec3)> {
|
||||
let point = |v: Vector3| DVec3::new(v.x, v.y, v.z);
|
||||
explode_dimension(dim, doc)
|
||||
.into_iter()
|
||||
.filter_map(|entity| match entity {
|
||||
EntityType::Line(line) => Some((point(line.start), point(line.end))),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// EXPLODE of a linear dimension in a working plane off world Z leaves
|
||||
/// its lines in that plane. Turned 90° about X, the dimension has to
|
||||
/// explode into the same lines turned with it; its offset, now along Z,
|
||||
/// used to be read in world XY only.
|
||||
#[test]
|
||||
fn exploding_a_linear_dimension_off_world_z_keeps_its_plane() {
|
||||
use crate::command::EntityTransform;
|
||||
let doc = CadDocument::new();
|
||||
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
|
||||
d.definition_point = Vector3::new(0.0, 5.0, 0.0);
|
||||
let flat = Dimension::Linear(d);
|
||||
let turn = glam::DQuat::from_axis_angle(DVec3::X, std::f64::consts::FRAC_PI_2);
|
||||
let expected: Vec<(DVec3, DVec3)> = exploded_segments(&flat, &doc)
|
||||
.into_iter()
|
||||
.map(|(a, b)| (turn * a, turn * b))
|
||||
.collect();
|
||||
assert!(!expected.is_empty(), "the dimension explodes into no lines");
|
||||
|
||||
let mut turned = EntityType::Dimension(flat);
|
||||
crate::scene::view::dispatch::apply_transform(
|
||||
&mut turned,
|
||||
&EntityTransform::Rotate {
|
||||
center: DVec3::ZERO,
|
||||
axis: DVec3::X,
|
||||
angle_rad: std::f64::consts::FRAC_PI_2,
|
||||
},
|
||||
);
|
||||
let EntityType::Dimension(turned) = turned else {
|
||||
panic!("dimension expected");
|
||||
};
|
||||
let lines = exploded_segments(&turned, &doc);
|
||||
assert_eq!(lines.len(), expected.len(), "line count");
|
||||
for (index, (got, want)) in lines.iter().zip(&expected).enumerate() {
|
||||
assert!(
|
||||
got.0.abs_diff_eq(want.0, 1e-9) && got.1.abs_diff_eq(want.1, 1e-9),
|
||||
"line {index}: expected {want:?}, got {got:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// An angular dimension must bake its swept ARC (not just two rays), else a
|
||||
// saved+reloaded angular dim collapses to a V. The block should carry the
|
||||
// two extension lines plus many arc chords.
|
||||
|
||||
@@ -34,6 +34,8 @@ pub struct SelectObjectsCommand {
|
||||
/// Whether the polygon being picked takes what it merely touches. Ignored
|
||||
/// by a fence, which has no inside to speak of.
|
||||
pick_crossing: bool,
|
||||
/// Add the command-specific Settings keyword used by Auto Constrain.
|
||||
auto_constrain_settings: bool,
|
||||
}
|
||||
|
||||
impl SelectObjectsCommand {
|
||||
@@ -47,6 +49,7 @@ impl SelectObjectsCommand {
|
||||
show_options: true,
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +64,7 @@ impl SelectObjectsCommand {
|
||||
show_options: false,
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,8 +79,15 @@ impl SelectObjectsCommand {
|
||||
show_options: false,
|
||||
pick: None,
|
||||
pick_crossing: true,
|
||||
auto_constrain_settings: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_constrain(pending_cmd: &str) -> Self {
|
||||
let mut command = Self::new(pending_cmd);
|
||||
command.auto_constrain_settings = true;
|
||||
command
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for SelectObjectsCommand {
|
||||
@@ -101,6 +112,14 @@ impl CadCommand for SelectObjectsCommand {
|
||||
.into_owned();
|
||||
}
|
||||
if self.commit_on_enter && !self.handles.is_empty() {
|
||||
if self.auto_constrain_settings {
|
||||
return t!(
|
||||
"%{cmd} Select objects or [Settings] (%{count} selected, Enter to apply):",
|
||||
cmd = self.prompt_cmd,
|
||||
count = self.handles.len()
|
||||
)
|
||||
.into_owned();
|
||||
}
|
||||
t!(
|
||||
"%{cmd} Select objects (%{count} selected, Enter to apply):",
|
||||
cmd = self.prompt_cmd,
|
||||
@@ -108,6 +127,13 @@ impl CadCommand for SelectObjectsCommand {
|
||||
)
|
||||
.into_owned()
|
||||
} else {
|
||||
if self.auto_constrain_settings {
|
||||
return t!(
|
||||
"%{cmd} Select objects or [Settings]:",
|
||||
cmd = self.prompt_cmd
|
||||
)
|
||||
.into_owned();
|
||||
}
|
||||
t!("%{cmd} Select objects:", cmd = self.prompt_cmd).into_owned()
|
||||
}
|
||||
}
|
||||
@@ -130,7 +156,10 @@ impl CadCommand for SelectObjectsCommand {
|
||||
// the on-screen keyboard-less case needs.
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.commit_on_enter && self.show_options {
|
||||
vec![
|
||||
if self.auto_constrain_settings {
|
||||
return vec![CmdOption::new(t!("Settings").as_ref(), "S")];
|
||||
}
|
||||
let options = vec![
|
||||
CmdOption::new(t!("Window").as_ref(), "W"),
|
||||
CmdOption::new(t!("Crossing").as_ref(), "C"),
|
||||
CmdOption::new(t!("Fence").as_ref(), "F"),
|
||||
@@ -141,7 +170,8 @@ impl CadCommand for SelectObjectsCommand {
|
||||
CmdOption::new(t!("Remove").as_ref(), "R"),
|
||||
CmdOption::new(t!("Previous").as_ref(), "P"),
|
||||
CmdOption::new(t!("Last").as_ref(), "L"),
|
||||
]
|
||||
];
|
||||
options
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
@@ -181,6 +211,9 @@ impl CadCommand for SelectObjectsCommand {
|
||||
return None;
|
||||
}
|
||||
match text.trim().to_uppercase().as_str() {
|
||||
"S" | "SETTINGS" if self.auto_constrain_settings => {
|
||||
Some(CmdResult::OpenAutoConstrainSettings)
|
||||
}
|
||||
"F" | "FENCE" => {
|
||||
self.pick = Some(FencePick::fence());
|
||||
Some(CmdResult::NeedPoint)
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
//! CCONSTRAINT — manual Coincident constraint input.
|
||||
//! unlike the rest of this module's plain select-and-click constraints,
|
||||
//! Coincident addresses a *point* on an entity (an endpoint, a circle/arc
|
||||
//! center), not the whole entity, so it needs two point picks instead of a
|
||||
//! selection.
|
||||
//!
|
||||
//! A `CadCommand` has no document access (see `value.rs`'s module doc
|
||||
//! comment for the same constraint on `DCONSTRAINT`/`ACONSTRAINT`), so this
|
||||
//! command only accumulates the two raw points `on_point` receives and hands
|
||||
//! them back as `CmdResult::AddCoincidentConstraint` — the host resolves
|
||||
//! each point to a real `SketchRef` (`sketch_constraints::nearest_sketch_point`)
|
||||
//! and adds the constraint, the same split `ReassociateCenterMark` already
|
||||
//! uses for a similar reason.
|
||||
//! Interactive Coincident input: ordered point/curve picks, repeated
|
||||
//! point-to-curve placement, and Coincident-only automatic inference.
|
||||
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, CoincidentPick};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub mod coincident_tool {
|
||||
@@ -31,45 +21,272 @@ pub mod coincident_tool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone)]
|
||||
enum Step {
|
||||
First,
|
||||
FirstObject,
|
||||
Second(CoincidentPick),
|
||||
PointsOnCurve {
|
||||
curve: CoincidentPick,
|
||||
multiple: bool,
|
||||
},
|
||||
AutoConstrain {
|
||||
handles: Vec<Handle>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct CoincidentConstraintCommand {
|
||||
first: Option<DVec3>,
|
||||
step: Step,
|
||||
picked_entity: Option<EntityType>,
|
||||
}
|
||||
|
||||
impl CoincidentConstraintCommand {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
Self {
|
||||
step: Step::First,
|
||||
picked_entity: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_pick(handle: Option<Handle>, point: DVec3) -> CoincidentPick {
|
||||
CoincidentPick {
|
||||
handle,
|
||||
point,
|
||||
whole_curve: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn curve_pick(handle: Handle, point: DVec3) -> CoincidentPick {
|
||||
CoincidentPick {
|
||||
handle: Some(handle),
|
||||
point,
|
||||
whole_curve: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn picked_constraint_point(entity: &EntityType, point: DVec3) -> bool {
|
||||
crate::scene::parametric_constraints::is_parametric_point_near(
|
||||
entity,
|
||||
acadrust::types::Vector3::new(point.x, point.y, point.z),
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_curve(entity: &EntityType) -> bool {
|
||||
matches!(
|
||||
entity,
|
||||
EntityType::Line(_)
|
||||
| EntityType::LwPolyline(_)
|
||||
| EntityType::Polyline2D(_)
|
||||
| EntityType::Circle(_)
|
||||
| EntityType::Arc(_)
|
||||
| EntityType::Ellipse(_)
|
||||
| EntityType::Spline(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CoincidentConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"CCONSTRAINT"
|
||||
"GCCOINCIDENT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.first.is_none() {
|
||||
"COINCIDENT Specify first point:".to_string()
|
||||
} else {
|
||||
"COINCIDENT Specify second point:".to_string()
|
||||
match self.step.clone() {
|
||||
Step::First => {
|
||||
"COINCIDENT Select first point or [Object/Autoconstrain] <Object>:".to_string()
|
||||
}
|
||||
Step::FirstObject => "COINCIDENT Select object:".to_string(),
|
||||
Step::Second(_) => "COINCIDENT Select second point or object:".to_string(),
|
||||
Step::PointsOnCurve { multiple: false, .. } => {
|
||||
"COINCIDENT Select point or [Multiple]:".to_string()
|
||||
}
|
||||
Step::PointsOnCurve { multiple: true, .. } => {
|
||||
"COINCIDENT Select points to coincide with the first object (Enter = done):"
|
||||
.to_string()
|
||||
}
|
||||
Step::AutoConstrain { handles } if handles.is_empty() => {
|
||||
"COINCIDENT Select objects:".to_string()
|
||||
}
|
||||
Step::AutoConstrain { handles } => format!(
|
||||
"COINCIDENT Select objects ({} selected, Enter = apply):",
|
||||
handles.len()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.first {
|
||||
None => {
|
||||
self.first = Some(pt);
|
||||
match &self.step {
|
||||
Step::First => {
|
||||
self.step = Step::Second(Self::point_pick(None, pt));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(first) => CmdResult::AddCoincidentConstraint {
|
||||
point_a: first,
|
||||
point_b: pt,
|
||||
Step::FirstObject => CmdResult::NeedPoint,
|
||||
Step::Second(first) => CmdResult::AddCoincidentConstraint {
|
||||
first: *first,
|
||||
second: Self::point_pick(None, pt),
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::PointsOnCurve { curve, multiple } => CmdResult::AddCoincidentConstraint {
|
||||
first: *curve,
|
||||
second: Self::point_pick(None, pt),
|
||||
multiple: *multiple,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::AutoConstrain { .. } => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step.clone() {
|
||||
Step::First => vec![
|
||||
CmdOption::new("Object", "O"),
|
||||
CmdOption::new("Autoconstrain", "A"),
|
||||
],
|
||||
Step::PointsOnCurve { multiple: false, .. } => vec![
|
||||
CmdOption::new("Point", "P"),
|
||||
CmdOption::new("Multiple", "M"),
|
||||
],
|
||||
Step::PointsOnCurve { multiple: true, .. } => vec![CmdOption::enter("Done")],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
!matches!(&self.step, Step::AutoConstrain { .. })
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text
|
||||
.trim()
|
||||
.trim_start_matches('_')
|
||||
.to_ascii_uppercase();
|
||||
match (&mut self.step, keyword.as_str()) {
|
||||
(Step::First, "O" | "OBJECT") => {
|
||||
self.step = Step::FirstObject;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::First, "A" | "AUTOCONSTRAIN") => {
|
||||
self.step = Step::AutoConstrain { handles: Vec::new() };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::PointsOnCurve { multiple, .. }, "M" | "MULTIPLE") => {
|
||||
*multiple = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::PointsOnCurve { .. }, "P" | "POINT") => Some(CmdResult::NeedPoint),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
!matches!(&self.step, Step::AutoConstrain { .. })
|
||||
}
|
||||
|
||||
fn entity_pick_accepts_points(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let is_point = self
|
||||
.picked_entity
|
||||
.as_ref()
|
||||
.is_some_and(|entity| Self::picked_constraint_point(entity, point));
|
||||
let is_curve = self
|
||||
.picked_entity
|
||||
.as_ref()
|
||||
.is_some_and(Self::valid_curve);
|
||||
self.picked_entity = None;
|
||||
|
||||
match self.step.clone() {
|
||||
Step::FirstObject if is_curve => {
|
||||
self.step = Step::PointsOnCurve {
|
||||
curve: Self::curve_pick(handle, point),
|
||||
multiple: false,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::First if is_point => {
|
||||
self.step = Step::Second(Self::point_pick(Some(handle), point));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::First if is_curve => {
|
||||
self.step = Step::PointsOnCurve {
|
||||
curve: Self::curve_pick(handle, point),
|
||||
multiple: false,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Second(first) if is_point => CmdResult::AddCoincidentConstraint {
|
||||
first,
|
||||
second: Self::point_pick(Some(handle), point),
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::Second(first) if is_curve => CmdResult::AddCoincidentConstraint {
|
||||
first,
|
||||
second: Self::curve_pick(handle, point),
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::PointsOnCurve { curve, multiple } if is_point => {
|
||||
CmdResult::AddCoincidentConstraint {
|
||||
first: curve,
|
||||
second: Self::point_pick(Some(handle), point),
|
||||
multiple,
|
||||
label: "Coincident constraint",
|
||||
}
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(&self.step, Step::AutoConstrain { .. })
|
||||
}
|
||||
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if let Step::AutoConstrain { handles: selected } = &mut self.step {
|
||||
*selected = handles;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if matches!(&self.step, Step::First) {
|
||||
self.step = Step::FirstObject;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if let Step::AutoConstrain { handles } = &self.step {
|
||||
if !handles.is_empty() {
|
||||
return CmdResult::AddAutoCoincidentConstraints {
|
||||
handles: handles.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
@@ -80,5 +297,63 @@ impl CadCommand for CoincidentConstraintCommand {
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["CCONSTRAINT"]
|
||||
names: &["CCONSTRAINT", "GCCOINCIDENT"]
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
#[test]
|
||||
fn multiple_keeps_the_first_curve_for_repeated_points() {
|
||||
let mut command = CoincidentConstraintCommand::new();
|
||||
assert!(matches!(
|
||||
command.on_text_input("Object"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
command.inject_picked_entity(EntityType::Line(
|
||||
acadrust::entities::Line::from_points(Vector3::ZERO, Vector3::UNIT_X),
|
||||
));
|
||||
let handle = Handle::new(7);
|
||||
assert!(matches!(
|
||||
command.on_entity_pick(handle, DVec3::ZERO),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_text_input("Multiple"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
command.on_point(DVec3::X),
|
||||
CmdResult::AddCoincidentConstraint {
|
||||
first: CoincidentPick {
|
||||
handle: Some(found),
|
||||
whole_curve: true,
|
||||
..
|
||||
},
|
||||
multiple: true,
|
||||
..
|
||||
} if found == handle
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn automatic_mode_returns_the_completed_selection() {
|
||||
let mut command = CoincidentConstraintCommand::new();
|
||||
assert!(matches!(
|
||||
command.on_text_input("Autoconstrain"),
|
||||
Some(CmdResult::NeedPoint)
|
||||
));
|
||||
let handles = vec![Handle::new(3), Handle::new(5)];
|
||||
assert!(matches!(
|
||||
command.on_selection_complete(handles.clone()),
|
||||
CmdResult::NeedPoint
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_enter(),
|
||||
CmdResult::AddAutoCoincidentConstraints { handles: found } if found == handles
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
|
||||
/// Command-line front end for the standard geometric-constraint family.
|
||||
/// Each choice is handed to the existing focused command so ribbon and typed
|
||||
/// entry use one implementation.
|
||||
pub struct GeomConstraintCommand;
|
||||
|
||||
impl GeomConstraintCommand {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn dispatch(keyword: &str) -> Option<&'static str> {
|
||||
Some(match keyword {
|
||||
"H" | "HORIZONTAL" => "HCONSTRAINT",
|
||||
"V" | "VERTICAL" => "VCONSTRAINT",
|
||||
"P" | "PERPENDICULAR" => "QCONSTRAINT",
|
||||
"PA" | "PARALLEL" => "PCONSTRAINT",
|
||||
"T" | "TANGENT" => "TCONSTRAINT",
|
||||
"SM" | "SMOOTH" => "SMOOTHCONSTRAINT",
|
||||
"C" | "COINCIDENT" => "GCCOINCIDENT",
|
||||
"CON" | "CONCENTRIC" => "NCONSTRAINT",
|
||||
"COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT",
|
||||
"SY" | "SYMMETRIC" => "SYCONSTRAINT",
|
||||
"E" | "EQUAL" => "ECONSTRAINT",
|
||||
"F" | "FIX" => "FXCONSTRAINT",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for GeomConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"GEOMCONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"GEOMCONSTRAINT Enter constraint type [Horizontal/Vertical/Perpendicular/Parallel/Tangent/Smooth/Coincident/Concentric/Collinear/Symmetric/Equal/Fix] <Coincident>:".to_string()
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
vec![
|
||||
CmdOption::new("Horizontal", "H"),
|
||||
CmdOption::new("Vertical", "V"),
|
||||
CmdOption::new("Perpendicular", "P"),
|
||||
CmdOption::new("Parallel", "PA"),
|
||||
CmdOption::new("Tangent", "T"),
|
||||
CmdOption::new("Smooth", "SM"),
|
||||
CmdOption::new("Coincident", "C"),
|
||||
CmdOption::new("Concentric", "CON"),
|
||||
CmdOption::new("Collinear", "COL"),
|
||||
CmdOption::new("Symmetric", "SY"),
|
||||
CmdOption::new("Equal", "E"),
|
||||
CmdOption::new("Fix", "F"),
|
||||
]
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text
|
||||
.trim()
|
||||
.trim_start_matches('_')
|
||||
.to_ascii_uppercase();
|
||||
Self::dispatch(&keyword).map(|command| CmdResult::Dispatch(command.to_string()))
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Dispatch("GCCOINCIDENT".to_string())
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["GEOMCONSTRAINT"]
|
||||
});
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chooser_dispatches_an_existing_constraint_command() {
|
||||
let mut command = GeomConstraintCommand::new();
|
||||
assert!(matches!(
|
||||
command.on_text_input("Parallel"),
|
||||
Some(CmdResult::Dispatch(found)) if found == "PCONSTRAINT"
|
||||
));
|
||||
assert!(matches!(
|
||||
command.on_enter(),
|
||||
CmdResult::Dispatch(found) if found == "GCCOINCIDENT"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
mod coincident;
|
||||
mod equal_distance;
|
||||
mod geom_constraint;
|
||||
mod point_on_entity;
|
||||
mod tools;
|
||||
mod value;
|
||||
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||
pub use geom_constraint::GeomConstraintCommand;
|
||||
pub use point_on_entity::{
|
||||
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
|
||||
};
|
||||
@@ -128,7 +130,7 @@ impl CadModule for ParametricModule {
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &[
|
||||
"AUTOCONSTRAIN", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET",
|
||||
"AUTOCONSTRAIN", "CONSTRAINTSETTINGS", "SMOOTHCONSTRAINT", "GCSHOW", "GCHIDE", "GCRESET",
|
||||
"GCSHOWALL", "GCHIDEALL", "DCSHOW", "DCHIDE", "DCSHOWALL", "DCHIDEALL",
|
||||
"DCCONVERT", "DELCONSTRAINT",
|
||||
]
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
//! endpoint) so it doesn't need interactive picking; the point side does
|
||||
//! (which of a line's two endpoints, or some other addressable point), so
|
||||
//! this command accumulates just that one point pick and hands it back for
|
||||
//! the host to resolve via `nearest_sketch_point` — the same split
|
||||
//! the host to resolve via `nearest_parametric_point` — the same split
|
||||
//! `CoincidentConstraintCommand` uses and for the same reason (`CadCommand`
|
||||
//! has no document access).
|
||||
|
||||
@@ -15,7 +15,7 @@ use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::sketch_constraints::ConstraintKind;
|
||||
use crate::scene::parametric_constraints::ConstraintKind;
|
||||
use acadrust::types::Handle;
|
||||
|
||||
pub mod center_point_tool {
|
||||
|
||||
@@ -64,9 +64,7 @@ pub mod equal {
|
||||
ToolDef {
|
||||
id: "ECONSTRAINT",
|
||||
label: "Equal",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/equal.svg"
|
||||
)),
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/equal.svg")),
|
||||
event: ModuleEvent::Command("ECONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -105,7 +103,7 @@ pub mod concentric {
|
||||
/// "the line passes through the circle's center" (a circle's radius is
|
||||
/// always normal to its own tangent), so it's built from the same
|
||||
/// `PointOnLine` primitive `PointOnCurve` already uses for a point-on-line
|
||||
/// case (`sketch_solve.rs`). Distinct from `Perpendicular`, which only
|
||||
/// case (`parametric_solve.rs`). Distinct from `Perpendicular`, which only
|
||||
/// covers line-to-line.
|
||||
pub mod normal {
|
||||
use super::*;
|
||||
@@ -113,9 +111,7 @@ pub mod normal {
|
||||
ToolDef {
|
||||
id: "NRCONSTRAINT",
|
||||
label: "Normal",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/normal.svg"
|
||||
)),
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/normal.svg")),
|
||||
event: ModuleEvent::Command("NRCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -141,9 +137,7 @@ pub mod fixed {
|
||||
ToolDef {
|
||||
id: "FXCONSTRAINT",
|
||||
label: "Fixed",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/fixed.svg"
|
||||
)),
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/fixed.svg")),
|
||||
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
+140
-155
@@ -4,8 +4,8 @@
|
||||
//!
|
||||
//! These commands don't solve anything themselves. A `CadCommand`'s `on_text_input` only
|
||||
//! gets `&mut self` — no document access — so it can't add a
|
||||
//! `SketchConstraint` to the scene directly; instead these commands hand the
|
||||
//! typed value back as `CmdResult::AddSketchConstraint`, and the host (which
|
||||
//! `ParametricConstraint` to the scene directly; instead these commands hand the
|
||||
//! typed value back as `CmdResult::AddParametricConstraint`, and the host (which
|
||||
//! does have `&mut Scene`) adds the record and solves it via the same
|
||||
//! `Scene::bump_entities` path any later edit to these entities will use too.
|
||||
//! `default_value` (what the prompt shows in `<...>`, and what a bare Enter
|
||||
@@ -19,7 +19,7 @@ use glam::DVec3;
|
||||
use crate::command::{CadCommand, CmdResult, InputKind};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::named_parameters::DrivingValue;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||
use crate::scene::Scene;
|
||||
|
||||
/// Recognizes a typed prompt token as either a numeric literal or a
|
||||
@@ -28,7 +28,7 @@ use crate::scene::Scene;
|
||||
/// ::new`, which already has `&Scene`) since `on_text_input` itself has no
|
||||
/// document access — the same constraint `default_value` already works
|
||||
/// around. Only *existence* is checked here; the actual value is resolved
|
||||
/// fresh at solve time (`sketch_solve::build_constraint`), consistent with
|
||||
/// fresh at solve time (`parametric_solve::build_constraint`), consistent with
|
||||
/// this project's "no incremental/cached resolution" approach throughout.
|
||||
fn parse_driving_value(text: &str, known_names: &[String]) -> Option<DrivingValue> {
|
||||
let text = text.trim();
|
||||
@@ -64,17 +64,70 @@ 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()) }
|
||||
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 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 {
|
||||
@@ -83,9 +136,7 @@ pub mod angle_tool {
|
||||
ToolDef {
|
||||
id: "ACONSTRAINT",
|
||||
label: "Angle",
|
||||
icon: IconKind::Svg(include_bytes!(
|
||||
"../../../assets/icons/constrain/angle.svg"
|
||||
)),
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/angle.svg")),
|
||||
event: ModuleEvent::Command("ACONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -104,14 +155,10 @@ pub enum DistanceMode {
|
||||
X,
|
||||
Y,
|
||||
Diameter,
|
||||
/// Arc length — only offered when `is_arc` (not a plain Circle, which
|
||||
/// has no "arc length" distinct from its circumference).
|
||||
ArcLength,
|
||||
}
|
||||
|
||||
/// Constrains a single line's length (or its X/Y-only component) between
|
||||
/// 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.
|
||||
/// its two endpoints, or a circle/arc's radius or diameter, to a typed target value.
|
||||
pub struct DistanceConstraintCommand {
|
||||
command_name: &'static str,
|
||||
handle: Handle,
|
||||
@@ -119,16 +166,10 @@ pub struct DistanceConstraintCommand {
|
||||
/// (Distance/DistanceX/DistanceY) — decided once at construction from
|
||||
/// the entity's type.
|
||||
is_circle: bool,
|
||||
/// Whether `handle` is specifically an Arc (as opposed to a full
|
||||
/// Circle) — gates the `[Arclength]` prompt option.
|
||||
is_arc: bool,
|
||||
mode: DistanceMode,
|
||||
default_value: f64,
|
||||
default_x: f64,
|
||||
default_y: f64,
|
||||
/// The entity's current arc length, for `ArcLength` mode's `<default>`
|
||||
/// — meaningless (left at 0) unless `is_arc`.
|
||||
default_arc_length: f64,
|
||||
/// Snapshot of `Scene::named_parameters`' names at construction time —
|
||||
/// see `parse_driving_value`'s doc comment for why a snapshot.
|
||||
known_param_names: Vec<String>,
|
||||
@@ -147,17 +188,16 @@ impl DistanceConstraintCommand {
|
||||
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 {
|
||||
EntityType::Line(l) => {
|
||||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
(false, false, (dx * dx + dy * dy).sqrt(), dx, dy, 0.0)
|
||||
}
|
||||
EntityType::Circle(c) => (true, false, c.radius, 0.0, 0.0, 0.0),
|
||||
EntityType::Arc(a) => (true, true, a.radius, 0.0, 0.0, a.arc_length()),
|
||||
_ => return None,
|
||||
};
|
||||
let (is_circle, default_value, default_x, default_y) = match entity {
|
||||
EntityType::Line(l) => {
|
||||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
(false, (dx * dx + dy * dy).sqrt(), dx, dy)
|
||||
}
|
||||
EntityType::Circle(c) => (true, c.radius, 0.0, 0.0),
|
||||
EntityType::Arc(a) => (true, a.radius, 0.0, 0.0),
|
||||
_ => return None,
|
||||
};
|
||||
let known_param_names = scene
|
||||
.named_parameters()
|
||||
.iter()
|
||||
@@ -165,7 +205,11 @@ impl DistanceConstraintCommand {
|
||||
.collect();
|
||||
let mode = match requested_mode {
|
||||
DistanceMode::Linear if !is_circle => {
|
||||
if default_x.abs() >= default_y.abs() { DistanceMode::X } else { DistanceMode::Y }
|
||||
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,
|
||||
@@ -178,12 +222,10 @@ impl DistanceConstraintCommand {
|
||||
command_name,
|
||||
handle,
|
||||
is_circle,
|
||||
is_arc,
|
||||
mode,
|
||||
default_value,
|
||||
default_x,
|
||||
default_y,
|
||||
default_arc_length,
|
||||
known_param_names,
|
||||
})
|
||||
}
|
||||
@@ -198,46 +240,41 @@ impl DistanceConstraintCommand {
|
||||
let (kind, refs, label) = match (self.is_circle, self.mode) {
|
||||
(true, DistanceMode::Diameter) => (
|
||||
ConstraintKind::Diameter,
|
||||
vec![SketchRef::whole(self.handle)],
|
||||
vec![ParametricRef::whole(self.handle)],
|
||||
"Diameter constraint",
|
||||
),
|
||||
(true, DistanceMode::ArcLength) if self.is_arc => (
|
||||
ConstraintKind::ArcLength,
|
||||
vec![SketchRef::whole(self.handle)],
|
||||
"Arc length constraint",
|
||||
),
|
||||
(true, DistanceMode::Auto | DistanceMode::Radius) => (
|
||||
ConstraintKind::Radius,
|
||||
vec![SketchRef::whole(self.handle)],
|
||||
vec![ParametricRef::whole(self.handle)],
|
||||
"Radius constraint",
|
||||
),
|
||||
(false, DistanceMode::X) => (
|
||||
ConstraintKind::DistanceX,
|
||||
vec![
|
||||
SketchRef::point(self.handle, 0),
|
||||
SketchRef::point(self.handle, 1),
|
||||
ParametricRef::point(self.handle, 0),
|
||||
ParametricRef::point(self.handle, 1),
|
||||
],
|
||||
"DistanceX constraint",
|
||||
),
|
||||
(false, DistanceMode::Y) => (
|
||||
ConstraintKind::DistanceY,
|
||||
vec![
|
||||
SketchRef::point(self.handle, 0),
|
||||
SketchRef::point(self.handle, 1),
|
||||
ParametricRef::point(self.handle, 0),
|
||||
ParametricRef::point(self.handle, 1),
|
||||
],
|
||||
"DistanceY constraint",
|
||||
),
|
||||
(false, DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear) => (
|
||||
ConstraintKind::Distance,
|
||||
vec![
|
||||
SketchRef::point(self.handle, 0),
|
||||
SketchRef::point(self.handle, 1),
|
||||
ParametricRef::point(self.handle, 0),
|
||||
ParametricRef::point(self.handle, 1),
|
||||
],
|
||||
"Distance constraint",
|
||||
),
|
||||
_ => return None,
|
||||
};
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind,
|
||||
refs,
|
||||
driving_param: Some(target),
|
||||
@@ -257,16 +294,6 @@ impl CadCommand for DistanceConstraintCommand {
|
||||
"Specify diameter <{:.4}> or [Radius]: ",
|
||||
self.default_value * 2.0
|
||||
),
|
||||
(true, DistanceMode::ArcLength) if self.is_arc => {
|
||||
format!(
|
||||
"Specify arc length <{:.4}> or [Radius]: ",
|
||||
self.default_arc_length
|
||||
)
|
||||
}
|
||||
(true, DistanceMode::Auto | DistanceMode::Radius) if self.is_arc => format!(
|
||||
"Specify distance <{:.4}> or [Diameter/Arclength]: ",
|
||||
self.default_value
|
||||
),
|
||||
(true, DistanceMode::Auto | DistanceMode::Radius) => format!(
|
||||
"Specify distance <{:.4}> or [Diameter]: ",
|
||||
self.default_value
|
||||
@@ -292,16 +319,24 @@ impl CadCommand for DistanceConstraintCommand {
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
let default = match self.mode {
|
||||
DistanceMode::Diameter => self.default_value * 2.0,
|
||||
DistanceMode::ArcLength => self.default_arc_length,
|
||||
DistanceMode::X => self.default_x,
|
||||
DistanceMode::Y => self.default_y,
|
||||
DistanceMode::Auto | DistanceMode::Aligned | DistanceMode::Linear | DistanceMode::Radius => self.default_value,
|
||||
DistanceMode::Auto
|
||||
| DistanceMode::Aligned
|
||||
| DistanceMode::Linear
|
||||
| DistanceMode::Radius => self.default_value,
|
||||
};
|
||||
self.build(DrivingValue::Literal(default))
|
||||
.unwrap_or(CmdResult::Cancel)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
// An existing parameter name wins over a mode keyword with the same spelling.
|
||||
if let Some(value @ DrivingValue::Named(_)) =
|
||||
parse_driving_value(text, &self.known_param_names)
|
||||
{
|
||||
return self.build(value);
|
||||
}
|
||||
// Mode-switch keywords re-prompt instead of building — consumed
|
||||
// inputs return `Some(NeedPoint)` so the driver doesn't re-offer
|
||||
// the same token a second time (matches e.g. `trim.rs`'s
|
||||
@@ -314,10 +349,6 @@ impl CadCommand for DistanceConstraintCommand {
|
||||
self.mode = DistanceMode::Diameter;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
(true, "A" | "ARCLENGTH") if self.is_arc => {
|
||||
self.mode = DistanceMode::ArcLength;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
(true, "R" | "RADIUS") => {
|
||||
self.mode = DistanceMode::Radius;
|
||||
return Some(CmdResult::NeedPoint);
|
||||
@@ -390,11 +421,11 @@ impl AngleConstraintCommand {
|
||||
if matches!(&target, DrivingValue::Literal(value) if !value.is_finite()) {
|
||||
return None;
|
||||
}
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Angle,
|
||||
refs: vec![
|
||||
SketchRef::whole(self.fixed_handle),
|
||||
SketchRef::whole(self.moving_handle),
|
||||
ParametricRef::whole(self.fixed_handle),
|
||||
ParametricRef::whole(self.moving_handle),
|
||||
],
|
||||
driving_param: Some(target),
|
||||
label: "Angle constraint",
|
||||
@@ -437,8 +468,15 @@ impl CadCommand for AngleConstraintCommand {
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &[
|
||||
"DCONSTRAINT", "ACONSTRAINT", "DCLINEAR", "DCHORIZONTAL", "DCVERTICAL",
|
||||
"DCALIGNED", "DCANGULAR", "DCRADIUS", "DCDIAMETER",
|
||||
"DCONSTRAINT",
|
||||
"ACONSTRAINT",
|
||||
"DCLINEAR",
|
||||
"DCHORIZONTAL",
|
||||
"DCVERTICAL",
|
||||
"DCALIGNED",
|
||||
"DCANGULAR",
|
||||
"DCRADIUS",
|
||||
"DCDIAMETER",
|
||||
]
|
||||
});
|
||||
|
||||
@@ -509,17 +547,6 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
fn add_arc(scene: &mut Scene) -> Handle {
|
||||
scene.add_entity(EntityType::Arc(
|
||||
acadrust::entities::Arc::from_center_radius_angles(
|
||||
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
|
||||
3.0,
|
||||
0.0,
|
||||
std::f64::consts::FRAC_PI_2,
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn x_keyword_switches_mode_and_then_builds_a_distance_x_constraint() {
|
||||
let mut scene = Scene::new();
|
||||
@@ -537,7 +564,7 @@ mod tests {
|
||||
);
|
||||
|
||||
match cmd.on_text_input("10") {
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind,
|
||||
driving_param: Some(DrivingValue::Literal(v)),
|
||||
..
|
||||
@@ -545,7 +572,7 @@ mod tests {
|
||||
assert_eq!(kind, ConstraintKind::DistanceX);
|
||||
assert_eq!(v, 10.0);
|
||||
}
|
||||
_ => panic!("expected an AddSketchConstraint{{DistanceX}}, got a different result"),
|
||||
_ => panic!("expected an AddParametricConstraint{{DistanceX}}, got a different result"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,7 +585,7 @@ mod tests {
|
||||
assert!(matches!(cmd.on_text_input("X"), Some(CmdResult::NeedPoint)));
|
||||
assert!(matches!(
|
||||
cmd.on_text_input("-3"),
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::DistanceX,
|
||||
driving_param: Some(DrivingValue::Literal(-3.0)),
|
||||
..
|
||||
@@ -576,7 +603,7 @@ mod tests {
|
||||
assert!(cmd.prompt().contains("<8.0000>"));
|
||||
assert!(matches!(
|
||||
cmd.on_enter(),
|
||||
CmdResult::AddSketchConstraint {
|
||||
CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::DistanceY,
|
||||
driving_param: Some(DrivingValue::Literal(8.0)),
|
||||
..
|
||||
@@ -584,6 +611,24 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_parameter_named_like_a_mode_keyword_remains_usable() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene);
|
||||
scene.named_parameters_mut().set("r", "5").unwrap();
|
||||
let mut cmd =
|
||||
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
|
||||
|
||||
assert!(matches!(
|
||||
cmd.on_text_input("R"),
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Radius,
|
||||
driving_param: Some(DrivingValue::Named(name)),
|
||||
..
|
||||
}) if name == "r"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordinary_distances_reject_nonpositive_and_nonfinite_values() {
|
||||
let mut scene = Scene::new();
|
||||
@@ -609,7 +654,7 @@ mod tests {
|
||||
);
|
||||
|
||||
match cmd.on_text_input("16") {
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
Some(CmdResult::AddParametricConstraint {
|
||||
kind,
|
||||
driving_param: Some(DrivingValue::Literal(v)),
|
||||
..
|
||||
@@ -617,7 +662,7 @@ mod tests {
|
||||
assert_eq!(kind, ConstraintKind::Diameter);
|
||||
assert_eq!(v, 16.0);
|
||||
}
|
||||
_ => panic!("expected an AddSketchConstraint{{Diameter}}"),
|
||||
_ => panic!("expected an AddParametricConstraint{{Diameter}}"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -629,70 +674,10 @@ mod tests {
|
||||
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
|
||||
|
||||
match cmd.on_text_input("5") {
|
||||
Some(CmdResult::AddSketchConstraint { kind, .. }) => {
|
||||
Some(CmdResult::AddParametricConstraint { kind, .. }) => {
|
||||
assert_eq!(kind, ConstraintKind::Radius)
|
||||
}
|
||||
_ => panic!("expected an AddSketchConstraint{{Radius}}"),
|
||||
_ => panic!("expected an AddParametricConstraint{{Radius}}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_keyword_switches_an_arcs_command_to_arc_length_mode() {
|
||||
let mut scene = Scene::new();
|
||||
let arc = add_arc(&mut scene);
|
||||
let mut cmd =
|
||||
DistanceConstraintCommand::new(&scene, arc).expect("Arc supports DCONSTRAINT");
|
||||
|
||||
assert!(matches!(cmd.on_text_input("A"), Some(CmdResult::NeedPoint)));
|
||||
assert!(
|
||||
cmd.prompt().contains("arc length"),
|
||||
"prompt should reflect ArcLength mode: {}",
|
||||
cmd.prompt()
|
||||
);
|
||||
|
||||
match cmd.on_text_input("8") {
|
||||
Some(CmdResult::AddSketchConstraint {
|
||||
kind,
|
||||
driving_param: Some(DrivingValue::Literal(v)),
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(kind, ConstraintKind::ArcLength);
|
||||
assert_eq!(v, 8.0);
|
||||
}
|
||||
_ => panic!("expected an AddSketchConstraint{{ArcLength}}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_length_keyword_is_not_offered_for_a_plain_circle() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene);
|
||||
let mut cmd =
|
||||
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
|
||||
|
||||
assert!(
|
||||
!cmd.prompt().contains("Arclength"),
|
||||
"a plain circle has no arc length option: {}",
|
||||
cmd.prompt()
|
||||
);
|
||||
// "A" isn't a recognized keyword here and doesn't parse as a
|
||||
// number or a named parameter either, so it's rejected outright.
|
||||
assert!(cmd.on_text_input("A").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arc_length_default_is_the_arcs_current_swept_length() {
|
||||
let mut scene = Scene::new();
|
||||
let arc = add_arc(&mut scene); // radius 3, 90° sweep -> length = 3 * pi/2
|
||||
let mut cmd =
|
||||
DistanceConstraintCommand::new(&scene, arc).expect("Arc supports DCONSTRAINT");
|
||||
cmd.on_text_input("A");
|
||||
|
||||
let expected = 3.0 * std::f64::consts::FRAC_PI_2;
|
||||
assert!(
|
||||
cmd.prompt().contains(&format!("{expected:.4}")),
|
||||
"prompt should default to the arc's current length: {}",
|
||||
cmd.prompt()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user