Harden parametric sketch constraints

This commit is contained in:
Hakan Seven committed 2026-09-11 09:45:57 +03:00
1 parent 41c07e9858
commit 9b58ca8a93
101 files changed
+3128 -13767

No files matched your search

+12 -4
View File
@@ -1,4 +1,4 @@
//! CCONSTRAINT — the manual Coincident UI (design doc §6.1's open item):
//! 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
@@ -23,7 +23,9 @@ pub mod coincident_tool {
ToolDef {
id: "CCONSTRAINT",
label: "Coincident",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/coincident.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/coincident.svg"
)),
event: ModuleEvent::Command("CCONSTRAINT".to_string()),
}
}
@@ -59,7 +61,11 @@ impl CadCommand for CoincidentConstraintCommand {
self.first = Some(pt);
CmdResult::NeedPoint
}
Some(first) => CmdResult::AddCoincidentConstraint { point_a: first, point_b: pt, label: "Coincident constraint" },
Some(first) => CmdResult::AddCoincidentConstraint {
point_a: first,
point_b: pt,
label: "Coincident constraint",
},
}
}
@@ -73,4 +79,6 @@ impl CadCommand for CoincidentConstraintCommand {
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["CCONSTRAINT"] });
inventory::submit!(crate::command::CommandRegistration {
names: &["CCONSTRAINT"]
});
+15 -7
View File
@@ -1,6 +1,5 @@
//! EDCONSTRAINT — AutoCAD's fourth `Equal` sub-kind
//! (`ACEQUALDISTANCECONSTRAINT`): the distance between one point pair
//! equals the distance between another, rather than two whole entities
//! EDCONSTRAINT makes the distance between one point pair equal the
//! distance between another pair, rather than comparing whole entities.
//! being equal length/radius. Needs four point picks (unlike the two
//! whole-entity selections `ECONSTRAINT` uses), so — like
//! `CoincidentConstraintCommand` — it accumulates raw points and hands them
@@ -17,7 +16,9 @@ pub mod equal_distance_tool {
ToolDef {
id: "EDCONSTRAINT",
label: "Equal Distance",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/equal_distance.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/equal_distance.svg"
)),
event: ModuleEvent::Command("EDCONSTRAINT".to_string()),
}
}
@@ -53,8 +54,13 @@ impl CadCommand for EqualDistanceConstraintCommand {
if self.points.len() < 4 {
return CmdResult::NeedPoint;
}
let [a, b, c, d] = self.points[..] else { unreachable!("just checked len == 4") };
CmdResult::AddEqualDistanceConstraint { points: [a, b, c, d], label: "Equal distance constraint" }
let [a, b, c, d] = self.points[..] else {
unreachable!("just checked len == 4")
};
CmdResult::AddEqualDistanceConstraint {
points: [a, b, c, d],
label: "Equal distance constraint",
}
}
fn on_enter(&mut self) -> CmdResult {
@@ -67,4 +73,6 @@ impl CadCommand for EqualDistanceConstraintCommand {
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["EDCONSTRAINT"] });
inventory::submit!(crate::command::CommandRegistration {
names: &["EDCONSTRAINT"]
});
+6 -4
View File
@@ -1,5 +1,4 @@
//! Ribbon tools and typed-value commands for the persistent "Constraints"
//! group (design doc `docs/parametric_system_design.md` §6.1).
//! Ribbon tools and typed-value commands for persistent constraints.
//!
//! Every one adds a [`crate::scene::sketch_constraints::SketchConstraint`]
//! to the current scope and lets `Scene::bump_entities` solve it, the same
@@ -29,8 +28,11 @@ mod tools;
mod value;
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
pub use point_on_entity::{center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand};
pub use point_on_entity::{
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
};
pub use tools::{
concentric, equal, fixed, horizontal, colinear, normal, parallel, perpendicular, symmetric, tangent, vertical,
colinear, concentric, equal, fixed, horizontal, normal, parallel, perpendicular, symmetric,
tangent, vertical,
};
pub use value::{angle_tool, distance_tool, AngleConstraintCommand, DistanceConstraintCommand};
+30 -7
View File
@@ -24,7 +24,9 @@ pub mod center_point_tool {
ToolDef {
id: "CPCONSTRAINT",
label: "Center Point",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/center_point.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/center_point.svg"
)),
event: ModuleEvent::Command("CPCONSTRAINT".to_string()),
}
}
@@ -36,7 +38,9 @@ pub mod midpoint_tool {
ToolDef {
id: "MPCONSTRAINT",
label: "Midpoint",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/midpoint.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/midpoint.svg"
)),
event: ModuleEvent::Command("MPCONSTRAINT".to_string()),
}
}
@@ -48,7 +52,9 @@ pub mod point_on_curve_tool {
ToolDef {
id: "OCCONSTRAINT",
label: "Point on Curve",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/point_on_curve.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/point_on_curve.svg"
)),
event: ModuleEvent::Command("OCCONSTRAINT".to_string()),
}
}
@@ -63,8 +69,18 @@ pub struct PointOnEntityConstraintCommand {
}
impl PointOnEntityConstraintCommand {
pub fn new(name: &'static str, kind: ConstraintKind, target: Handle, label: &'static str) -> Self {
Self { name, kind, target, label }
pub fn new(
name: &'static str,
kind: ConstraintKind,
target: Handle,
label: &'static str,
) -> Self {
Self {
name,
kind,
target,
label,
}
}
}
@@ -78,7 +94,12 @@ impl CadCommand for PointOnEntityConstraintCommand {
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
CmdResult::AddPointOnEntityConstraint { point: pt, target: self.target, kind: self.kind, label: self.label }
CmdResult::AddPointOnEntityConstraint {
point: pt,
target: self.target,
kind: self.kind,
label: self.label,
}
}
fn on_enter(&mut self) -> CmdResult {
@@ -91,4 +112,6 @@ impl CadCommand for PointOnEntityConstraintCommand {
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["CPCONSTRAINT", "MPCONSTRAINT", "OCCONSTRAINT"] });
inventory::submit!(crate::command::CommandRegistration {
names: &["CPCONSTRAINT", "MPCONSTRAINT", "OCCONSTRAINT"]
});
+34 -13
View File
@@ -8,7 +8,9 @@ pub mod horizontal {
ToolDef {
id: "HCONSTRAINT",
label: "Horizontal",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/horizontal.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/horizontal.svg"
)),
event: ModuleEvent::Command("HCONSTRAINT".to_string()),
}
}
@@ -20,7 +22,9 @@ pub mod vertical {
ToolDef {
id: "VCONSTRAINT",
label: "Vertical",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/vertical.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/vertical.svg"
)),
event: ModuleEvent::Command("VCONSTRAINT".to_string()),
}
}
@@ -32,7 +36,9 @@ pub mod parallel {
ToolDef {
id: "PCONSTRAINT",
label: "Parallel",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/parallel.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/parallel.svg"
)),
event: ModuleEvent::Command("PCONSTRAINT".to_string()),
}
}
@@ -44,7 +50,9 @@ pub mod perpendicular {
ToolDef {
id: "QCONSTRAINT",
label: "Perpendicular",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/perpendicular.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/perpendicular.svg"
)),
event: ModuleEvent::Command("QCONSTRAINT".to_string()),
}
}
@@ -56,7 +64,9 @@ 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()),
}
}
@@ -68,7 +78,9 @@ pub mod tangent {
ToolDef {
id: "TCONSTRAINT",
label: "Tangent",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/tangent.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/tangent.svg"
)),
event: ModuleEvent::Command("TCONSTRAINT".to_string()),
}
}
@@ -80,7 +92,9 @@ pub mod concentric {
ToolDef {
id: "NCONSTRAINT",
label: "Concentric",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/concentric.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/concentric.svg"
)),
event: ModuleEvent::Command("NCONSTRAINT".to_string()),
}
}
@@ -92,15 +106,16 @@ pub mod concentric {
/// 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
/// covers line-to-line — AutoCAD's own `GeomConstraintType` enum lists
/// `kNormal` and `kPerpendicular` separately for exactly this reason.
/// covers line-to-line.
pub mod normal {
use super::*;
pub fn tool() -> ToolDef {
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()),
}
}
@@ -112,7 +127,9 @@ pub mod colinear {
ToolDef {
id: "LCONSTRAINT",
label: "Colinear",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/colinear.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/colinear.svg"
)),
event: ModuleEvent::Command("LCONSTRAINT".to_string()),
}
}
@@ -124,7 +141,9 @@ 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()),
}
}
@@ -136,7 +155,9 @@ pub mod symmetric {
ToolDef {
id: "SYCONSTRAINT",
label: "Symmetric",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/symmetric.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/symmetric.svg"
)),
event: ModuleEvent::Command("SYCONSTRAINT".to_string()),
}
}
+257 -80
View File
@@ -2,8 +2,7 @@
//! (a distance or an angle), unlike the plain select-and-click constraints
//! in `mod.rs`.
//!
//! Persistent (design doc `docs/parametric_system_design.md` §6.1): these
//! don't solve anything themselves. A `CadCommand`'s `on_text_input` only
//! 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
@@ -24,11 +23,7 @@ use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
use crate::scene::Scene;
/// Recognizes a typed prompt token as either a numeric literal or a
/// reference to an existing named parameter (`docs/
/// named_parameters_design.md` stage 4's "a way to pick a named parameter
/// instead of typing a literal" — the same prompt takes either, mirroring
/// how AutoCAD's own dimensional-constraint prompts accept an expression in
/// place of a bare value). `known_names` is a one-time snapshot taken at
/// reference to an existing named parameter. `known_names` is a one-time snapshot taken at
/// command construction (`DistanceConstraintCommand`/`AngleConstraintCommand
/// ::new`, which already has `&Scene`) since `on_text_input` itself has no
/// document access — the same constraint `default_value` already works
@@ -57,7 +52,9 @@ pub mod distance_tool {
ToolDef {
id: "DCONSTRAINT",
label: "Distance",
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/constrain/distance.svg")),
icon: IconKind::Svg(include_bytes!(
"../../../../assets/icons/constrain/distance.svg"
)),
event: ModuleEvent::Command("DCONSTRAINT".to_string()),
}
}
@@ -69,17 +66,15 @@ 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()),
}
}
}
/// Which flavor of dimensional constraint the typed value ultimately
/// drives — selectable via a prompt keyword before the value itself is
/// typed, mirroring how real AutoCAD's own `DCCONSTRAINT` offers
/// `[Xdistance/Ydistance]` or `[Diameter]` as options on the same command
/// rather than as separate tools.
/// Which flavor of dimensional constraint the typed value drives.
#[derive(Clone, Copy, PartialEq, Eq)]
enum DistanceMode {
/// Line length (two-point distance) or a circle/arc's radius —
@@ -107,6 +102,8 @@ pub struct DistanceConstraintCommand {
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,
@@ -119,49 +116,89 @@ impl DistanceConstraintCommand {
/// `None` if `handle` isn't a Line, Circle, or Arc.
pub fn new(scene: &Scene, handle: Handle) -> Option<Self> {
let entity = scene.document.get_entity(handle)?;
let (is_circle, is_arc, default_value, 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(), 0.0)
}
EntityType::Circle(c) => (true, false, c.radius, 0.0),
EntityType::Arc(a) => (true, true, a.radius, a.arc_length()),
_ => return None,
};
let known_param_names = scene.named_parameters().iter().map(|p| p.name.clone()).collect();
Some(Self { handle, is_circle, is_arc, mode: DistanceMode::Auto, default_value, default_arc_length, known_param_names })
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 known_param_names = scene
.named_parameters()
.iter()
.map(|p| p.name.clone())
.collect();
Some(Self {
handle,
is_circle,
is_arc,
mode: DistanceMode::Auto,
default_value,
default_x,
default_y,
default_arc_length,
known_param_names,
})
}
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
if let DrivingValue::Literal(v) = target {
if v <= 0.0 {
if let DrivingValue::Literal(v) = &target {
let signed_component = matches!(self.mode, DistanceMode::X | DistanceMode::Y);
if !v.is_finite() || (!signed_component && *v <= 0.0) {
return None;
}
}
let (kind, refs, label) = match (self.is_circle, self.mode) {
(true, DistanceMode::Diameter) => (ConstraintKind::Diameter, vec![SketchRef::whole(self.handle)], "Diameter constraint"),
(true, DistanceMode::ArcLength) if self.is_arc => {
(ConstraintKind::ArcLength, vec![SketchRef::whole(self.handle)], "Arc length constraint")
}
(true, _) => (ConstraintKind::Radius, vec![SketchRef::whole(self.handle)], "Radius constraint"),
(true, DistanceMode::Diameter) => (
ConstraintKind::Diameter,
vec![SketchRef::whole(self.handle)],
"Diameter constraint",
),
(true, DistanceMode::ArcLength) if self.is_arc => (
ConstraintKind::ArcLength,
vec![SketchRef::whole(self.handle)],
"Arc length constraint",
),
(true, _) => (
ConstraintKind::Radius,
vec![SketchRef::whole(self.handle)],
"Radius constraint",
),
(false, DistanceMode::X) => (
ConstraintKind::DistanceX,
vec![SketchRef::point(self.handle, 0), SketchRef::point(self.handle, 1)],
vec![
SketchRef::point(self.handle, 0),
SketchRef::point(self.handle, 1),
],
"DistanceX constraint",
),
(false, DistanceMode::Y) => (
ConstraintKind::DistanceY,
vec![SketchRef::point(self.handle, 0), SketchRef::point(self.handle, 1)],
vec![
SketchRef::point(self.handle, 0),
SketchRef::point(self.handle, 1),
],
"DistanceY constraint",
),
(false, _) => (
ConstraintKind::Distance,
vec![SketchRef::point(self.handle, 0), SketchRef::point(self.handle, 1)],
vec![
SketchRef::point(self.handle, 0),
SketchRef::point(self.handle, 1),
],
"Distance constraint",
),
};
Some(CmdResult::AddSketchConstraint { kind, refs, driving_param: Some(target), label })
Some(CmdResult::AddSketchConstraint {
kind,
refs,
driving_param: Some(target),
label,
})
}
}
@@ -172,15 +209,30 @@ impl CadCommand for DistanceConstraintCommand {
fn prompt(&self) -> String {
match (self.is_circle, self.mode) {
(true, DistanceMode::Diameter) => format!("Specify diameter <{:.4}> or [Radius]: ", self.default_value * 2.0),
(true, DistanceMode::Diameter) => format!(
"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)
format!(
"Specify arc length <{:.4}> or [Radius]: ",
self.default_arc_length
)
}
(true, _) if self.is_arc => format!("Specify distance <{:.4}> or [Diameter/Arclength]: ", self.default_value),
(true, _) => format!("Specify distance <{:.4}> or [Diameter]: ", self.default_value),
(false, DistanceMode::X) => format!("Specify X distance <{:.4}>: ", self.default_value),
(false, DistanceMode::Y) => format!("Specify Y distance <{:.4}>: ", self.default_value),
(false, _) => format!("Specify distance <{:.4}> or [Xdistance/Ydistance]: ", self.default_value),
(true, _) if self.is_arc => format!(
"Specify distance <{:.4}> or [Diameter/Arclength]: ",
self.default_value
),
(true, _) => 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!(
"Specify distance <{:.4}> or [Xdistance/Ydistance]: ",
self.default_value
),
}
}
@@ -196,9 +248,12 @@ impl CadCommand for DistanceConstraintCommand {
let default = match self.mode {
DistanceMode::Diameter => self.default_value * 2.0,
DistanceMode::ArcLength if self.is_arc => self.default_arc_length,
DistanceMode::X => self.default_x,
DistanceMode::Y => self.default_y,
_ => self.default_value,
};
self.build(DrivingValue::Literal(default)).unwrap_or(CmdResult::Cancel)
self.build(DrivingValue::Literal(default))
.unwrap_or(CmdResult::Cancel)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
@@ -262,14 +317,29 @@ impl AngleConstraintCommand {
let a1 = (f.end.y - f.start.y).atan2(f.end.x - f.start.x);
let a2 = (m.end.y - m.start.y).atan2(m.end.x - m.start.x);
let default_value = (a2 - a1).to_degrees();
let known_param_names = scene.named_parameters().iter().map(|p| p.name.clone()).collect();
Some(Self { fixed_handle: fixed, moving_handle: moving, default_value, known_param_names })
let known_param_names = scene
.named_parameters()
.iter()
.map(|p| p.name.clone())
.collect();
Some(Self {
fixed_handle: fixed,
moving_handle: moving,
default_value,
known_param_names,
})
}
fn build(&self, target: DrivingValue) -> Option<CmdResult> {
if matches!(&target, DrivingValue::Literal(value) if !value.is_finite()) {
return None;
}
Some(CmdResult::AddSketchConstraint {
kind: ConstraintKind::Angle,
refs: vec![SketchRef::whole(self.fixed_handle), SketchRef::whole(self.moving_handle)],
refs: vec![
SketchRef::whole(self.fixed_handle),
SketchRef::whole(self.moving_handle),
],
driving_param: Some(target),
label: "Angle constraint",
})
@@ -294,7 +364,8 @@ impl CadCommand for AngleConstraintCommand {
}
fn on_enter(&mut self) -> CmdResult {
self.build(DrivingValue::Literal(self.default_value)).unwrap_or(CmdResult::Cancel)
self.build(DrivingValue::Literal(self.default_value))
.unwrap_or(CmdResult::Cancel)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
@@ -308,7 +379,9 @@ impl CadCommand for AngleConstraintCommand {
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["DCONSTRAINT", "ACONSTRAINT"] });
inventory::submit!(crate::command::CommandRegistration {
names: &["DCONSTRAINT", "ACONSTRAINT"]
});
#[cfg(test)]
mod tests {
@@ -317,16 +390,25 @@ mod tests {
#[test]
fn parse_driving_value_prefers_a_numeric_literal() {
let known = vec!["hole_dia".to_string()];
assert_eq!(parse_driving_value("12.5", &known), Some(DrivingValue::Literal(12.5)));
assert_eq!(
parse_driving_value("12.5", &known),
Some(DrivingValue::Literal(12.5))
);
// Distance's own `build` rejects a non-positive target later; the
// parse step itself accepts any number, negative included.
assert_eq!(parse_driving_value("-3", &known), Some(DrivingValue::Literal(-3.0)));
assert_eq!(
parse_driving_value("-3", &known),
Some(DrivingValue::Literal(-3.0))
);
}
#[test]
fn parse_driving_value_recognizes_a_known_parameter_name() {
let known = vec!["hole_dia".to_string(), "plate_len".to_string()];
assert_eq!(parse_driving_value("hole_dia", &known), Some(DrivingValue::Named("hole_dia".to_string())));
assert_eq!(
parse_driving_value("hole_dia", &known),
Some(DrivingValue::Named("hole_dia".to_string()))
);
}
#[test]
@@ -342,8 +424,14 @@ mod tests {
// here, so a lowercase-named parameter must still resolve — and
// resolve to its canonically-cased name, not the uppercased token.
let known = vec!["hole_dia".to_string()];
assert_eq!(parse_driving_value("HOLE_DIA", &known), Some(DrivingValue::Named("hole_dia".to_string())));
assert_eq!(parse_driving_value("Hole_Dia", &known), Some(DrivingValue::Named("hole_dia".to_string())));
assert_eq!(
parse_driving_value("HOLE_DIA", &known),
Some(DrivingValue::Named("hole_dia".to_string()))
);
assert_eq!(
parse_driving_value("Hole_Dia", &known),
Some(DrivingValue::Named("hole_dia".to_string()))
);
}
fn add_line(scene: &mut Scene) -> Handle {
@@ -354,34 +442,47 @@ mod tests {
}
fn add_circle(scene: &mut Scene) -> Handle {
scene.add_entity(EntityType::Circle(acadrust::entities::Circle::from_center_radius(
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
3.0,
)))
scene.add_entity(EntityType::Circle(
acadrust::entities::Circle::from_center_radius(
acadrust::types::Vector3::new(0.0, 0.0, 0.0),
3.0,
),
))
}
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,
)))
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();
let line = add_line(&mut scene);
let mut cmd = DistanceConstraintCommand::new(&scene, line).expect("Line supports DCONSTRAINT");
let mut cmd =
DistanceConstraintCommand::new(&scene, line).expect("Line supports DCONSTRAINT");
// The keyword itself only switches mode and re-prompts — it must
// not build a constraint yet.
assert!(matches!(cmd.on_text_input("X"), Some(CmdResult::NeedPoint)));
assert!(cmd.prompt().contains("X distance"), "prompt should reflect the new mode: {}", cmd.prompt());
assert!(
cmd.prompt().contains("X distance"),
"prompt should reflect the new mode: {}",
cmd.prompt()
);
match cmd.on_text_input("10") {
Some(CmdResult::AddSketchConstraint { kind, driving_param: Some(DrivingValue::Literal(v)), .. }) => {
Some(CmdResult::AddSketchConstraint {
kind,
driving_param: Some(DrivingValue::Literal(v)),
..
}) => {
assert_eq!(kind, ConstraintKind::DistanceX);
assert_eq!(v, 10.0);
}
@@ -389,17 +490,71 @@ mod tests {
}
}
#[test]
fn component_distances_accept_signed_values() {
let mut scene = Scene::new();
let line = add_line(&mut scene);
let mut cmd =
DistanceConstraintCommand::new(&scene, line).expect("Line supports DCONSTRAINT");
assert!(matches!(cmd.on_text_input("X"), Some(CmdResult::NeedPoint)));
assert!(matches!(
cmd.on_text_input("-3"),
Some(CmdResult::AddSketchConstraint {
kind: ConstraintKind::DistanceX,
driving_param: Some(DrivingValue::Literal(-3.0)),
..
})
));
}
#[test]
fn component_distance_defaults_use_the_selected_axis() {
let mut scene = Scene::new();
let line = add_line(&mut scene);
let mut cmd =
DistanceConstraintCommand::new(&scene, line).expect("Line supports DCONSTRAINT");
assert!(matches!(cmd.on_text_input("Y"), Some(CmdResult::NeedPoint)));
assert!(cmd.prompt().contains("<8.0000>"));
assert!(matches!(
cmd.on_enter(),
CmdResult::AddSketchConstraint {
kind: ConstraintKind::DistanceY,
driving_param: Some(DrivingValue::Literal(8.0)),
..
}
));
}
#[test]
fn ordinary_distances_reject_nonpositive_and_nonfinite_values() {
let mut scene = Scene::new();
let line = add_line(&mut scene);
let mut cmd =
DistanceConstraintCommand::new(&scene, line).expect("Line supports DCONSTRAINT");
assert!(cmd.on_text_input("-3").is_none());
assert!(cmd.build(DrivingValue::Literal(f64::INFINITY)).is_none());
}
#[test]
fn d_keyword_switches_a_circles_command_to_diameter_mode() {
let mut scene = Scene::new();
let circle = add_circle(&mut scene);
let mut cmd = DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
let mut cmd =
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
assert!(matches!(cmd.on_text_input("D"), Some(CmdResult::NeedPoint)));
assert!(cmd.prompt().contains("diameter"), "prompt should reflect Diameter mode: {}", cmd.prompt());
assert!(
cmd.prompt().contains("diameter"),
"prompt should reflect Diameter mode: {}",
cmd.prompt()
);
match cmd.on_text_input("16") {
Some(CmdResult::AddSketchConstraint { kind, driving_param: Some(DrivingValue::Literal(v)), .. }) => {
Some(CmdResult::AddSketchConstraint {
kind,
driving_param: Some(DrivingValue::Literal(v)),
..
}) => {
assert_eq!(kind, ConstraintKind::Diameter);
assert_eq!(v, 16.0);
}
@@ -411,10 +566,13 @@ mod tests {
fn default_mode_still_builds_a_plain_radius_constraint() {
let mut scene = Scene::new();
let circle = add_circle(&mut scene);
let mut cmd = DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
let mut cmd =
DistanceConstraintCommand::new(&scene, circle).expect("Circle supports DCONSTRAINT");
match cmd.on_text_input("5") {
Some(CmdResult::AddSketchConstraint { kind, .. }) => assert_eq!(kind, ConstraintKind::Radius),
Some(CmdResult::AddSketchConstraint { kind, .. }) => {
assert_eq!(kind, ConstraintKind::Radius)
}
_ => panic!("expected an AddSketchConstraint{{Radius}}"),
}
}
@@ -423,13 +581,22 @@ mod tests {
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");
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());
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)), .. }) => {
Some(CmdResult::AddSketchConstraint {
kind,
driving_param: Some(DrivingValue::Literal(v)),
..
}) => {
assert_eq!(kind, ConstraintKind::ArcLength);
assert_eq!(v, 8.0);
}
@@ -441,9 +608,14 @@ mod tests {
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");
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());
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());
@@ -453,10 +625,15 @@ mod tests {
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");
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());
assert!(
cmd.prompt().contains(&format!("{expected:.4}")),
"prompt should default to the arc's current length: {}",
cmd.prompt()
);
}
}