forked from 1b-agent/opencadstudio-upstream
feat(ui): feature-parity context menu — Snap Overrides, transparent Pan/Zoom, ARC keyword flow
The command menu now follows the commercial solutions' layout row for row: Enter, Cancel,
Recent Input ▸, the command's keywords, Snap Overrides ▸ (every object snap
with its glyph, Mid Between 2 Points, None, Osnap Settings…), then Pan and
Zoom. Pan / Zoom run transparently while a command is active ('PAN,
'ZOOM …): navigation modes leave the command untouched and an interactive
ZOOM Window parks it and resumes it afterwards. Rows carry an icon gutter
so labels align; the Enter row shows a default value ("<0.5000>") as its
hint instead of a second finish row.
ARC gets the commercial solutions' prompt flow with all keywords: start [Center], second
point [Center/End], end; after Center: end [Angle/chord Length]; after
End: center [Angle/Direction/Radius]; Enter continues tangent. The
previous default (Center/Start/End with method codes) stays as ARC_CSE.
Object snaps gain a one-shot "None" override. New strings in all locales.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
1 parent
96df4928c7
commit
02cd98e790
34 files changed
+1305
-67
No files matched your search
@@ -274,7 +274,7 @@ fn end_angle_from_chord_len(start_angle: f64, chord: f64, r: f64) -> Option<f64>
|
||||
|
||||
// ── Command 1: Center, Start, End ─────────────────────────────────────────
|
||||
|
||||
pub struct ArcCommand {
|
||||
pub struct ArcCSECommand {
|
||||
step: u8,
|
||||
c: DVec3,
|
||||
r: f64,
|
||||
@@ -283,7 +283,7 @@ pub struct ArcCommand {
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcCommand {
|
||||
impl ArcCSECommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: 0,
|
||||
@@ -296,7 +296,7 @@ impl ArcCommand {
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcCommand {
|
||||
impl CadCommand for ArcCSECommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
@@ -411,6 +411,443 @@ impl CadCommand for ArcCommand {
|
||||
}
|
||||
}
|
||||
|
||||
// ── ARC: AutoCAD prompt flow ──────────────────────────────────────────────
|
||||
//
|
||||
// The plain ARC command follows AutoCAD's prompts, so every keyword a user
|
||||
// knows is there at the same step:
|
||||
// Specify start point of arc or [Center]
|
||||
// Specify second point of arc or [Center/End]
|
||||
// Specify end point of arc
|
||||
// … after Center: Specify end point of arc or [Angle/chord Length]
|
||||
// … after End: Specify center point of arc or [Angle/Direction/Radius]
|
||||
// Enter at the start prompt continues tangent from the last line/arc.
|
||||
// Holding Ctrl flips the sweep direction where it is not fixed by the picks.
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
enum ArcStep {
|
||||
Start,
|
||||
Second { s: DVec3 },
|
||||
End3 { s: DVec3, m: DVec3 },
|
||||
CenterFirst,
|
||||
StartAfterCenter { c: DVec3 },
|
||||
CenterAfterStart { s: DVec3 },
|
||||
/// Start and centre known: end point [Angle / chord Length].
|
||||
EndSC { s: DVec3, c: DVec3 },
|
||||
AngleSC { s: DVec3, c: DVec3 },
|
||||
LengthSC { s: DVec3, c: DVec3 },
|
||||
EndAfterStart { s: DVec3 },
|
||||
/// Start and end known: centre point [Angle / Direction / Radius].
|
||||
CenterSE { s: DVec3, e: DVec3 },
|
||||
AngleSE { s: DVec3, e: DVec3 },
|
||||
DirectionSE { s: DVec3, e: DVec3 },
|
||||
RadiusSE { s: DVec3, e: DVec3 },
|
||||
}
|
||||
|
||||
pub struct ArcCommand {
|
||||
step: ArcStep,
|
||||
cw: bool,
|
||||
plane: WorkingPlane,
|
||||
}
|
||||
|
||||
impl ArcCommand {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: ArcStep::Start,
|
||||
cw: false,
|
||||
plane: WorkingPlane::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Arc about `c` from `s` towards the direction of `e` (Ctrl flips).
|
||||
fn sc_end(&self, s: DVec3, c: DVec3, e: DVec3) -> Option<(DVec3, f64, f64, f64)> {
|
||||
let r = c.distance(s);
|
||||
if r < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let sa = angle_xy(c, s, self.plane);
|
||||
let ea = angle_xy(c, e, self.plane);
|
||||
Some(if self.cw { (c, r, ea, sa) } else { (c, r, sa, ea) })
|
||||
}
|
||||
|
||||
/// Arc about `c` from `s` sweeping the signed `span` (radians, CCW > 0).
|
||||
fn sc_angle(&self, s: DVec3, c: DVec3, span: f64) -> Option<(DVec3, f64, f64, f64)> {
|
||||
let r = c.distance(s);
|
||||
if r < 1e-9 || span.abs() < 1e-9 {
|
||||
return None;
|
||||
}
|
||||
let sa = angle_xy(c, s, self.plane);
|
||||
let ea = sa + span;
|
||||
Some(if span < 0.0 { (c, r, ea, sa) } else { (c, r, sa, ea) })
|
||||
}
|
||||
|
||||
/// Arc about `c` from `s` with chord `len` (Ctrl flips).
|
||||
fn sc_length(&self, s: DVec3, c: DVec3, len: f64) -> Option<(DVec3, f64, f64, f64)> {
|
||||
let r = c.distance(s);
|
||||
if r < 1e-9 || len == 0.0 {
|
||||
return None;
|
||||
}
|
||||
let sa = angle_xy(c, s, self.plane);
|
||||
let ea = end_angle_from_chord_len(sa, len, r)?;
|
||||
Some(if self.cw { (c, r, ea, sa) } else { (c, r, sa, ea) })
|
||||
}
|
||||
|
||||
/// Sweep from `s` to the cursor about `c`, going the way Ctrl says.
|
||||
fn cursor_span(&self, s: DVec3, c: DVec3, cursor: DVec3) -> f64 {
|
||||
let span = (angle_xy(c, cursor, self.plane) - angle_xy(c, s, self.plane)).rem_euclid(TAU);
|
||||
if self.cw {
|
||||
span - TAU
|
||||
} else {
|
||||
span
|
||||
}
|
||||
}
|
||||
|
||||
fn typed_number(text: &str) -> Option<f64> {
|
||||
crate::entities::common::parse_typed_length(text.trim())
|
||||
}
|
||||
|
||||
fn typed_angle(text: &str) -> Option<f64> {
|
||||
crate::entities::common::parse_typed_angle(text.trim())
|
||||
}
|
||||
|
||||
fn commit(&self, arc: Option<(DVec3, f64, f64, f64)>) -> CmdResult {
|
||||
match arc {
|
||||
Some((c, r, sa, ea)) => arc_result(c, r, sa, ea, self.plane),
|
||||
None => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn preview(&self, arc: Option<(DVec3, f64, f64, f64)>, fallback: WireModel) -> Option<WireModel> {
|
||||
match arc {
|
||||
Some((c, r, sa, ea)) => arc_preview(c, r, sa, ea, self.plane).or(Some(fallback)),
|
||||
None => Some(fallback),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for ArcCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
fn set_ctrl(&mut self, ctrl: bool) {
|
||||
self.cw = ctrl;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"ARC"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
ArcStep::Start => t!("ARC Specify start point of arc:"),
|
||||
ArcStep::Second { .. } => t!("ARC Specify second point of arc:"),
|
||||
ArcStep::End3 { .. } | ArcStep::EndSC { .. } | ArcStep::EndAfterStart { .. } => {
|
||||
t!("ARC Specify end point of arc:")
|
||||
}
|
||||
ArcStep::CenterFirst | ArcStep::CenterAfterStart { .. } | ArcStep::CenterSE { .. } => {
|
||||
t!("ARC Specify center point of arc:")
|
||||
}
|
||||
ArcStep::StartAfterCenter { .. } => t!("ARC Specify start point of arc:"),
|
||||
ArcStep::AngleSC { .. } | ArcStep::AngleSE { .. } => t!("ARC Specify included angle:"),
|
||||
ArcStep::LengthSC { .. } => t!("ARC Specify length of chord:"),
|
||||
ArcStep::DirectionSE { .. } => {
|
||||
t!("ARC Specify tangent direction for the start point of arc:")
|
||||
}
|
||||
ArcStep::RadiusSE { .. } => t!("ARC Specify radius of arc:"),
|
||||
}
|
||||
.into_owned()
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<crate::command::CmdOption> {
|
||||
use crate::command::CmdOption;
|
||||
match self.step {
|
||||
ArcStep::Start => vec![CmdOption::new("Center", "C")],
|
||||
ArcStep::Second { .. } => vec![CmdOption::new("Center", "C"), CmdOption::new("End", "E")],
|
||||
ArcStep::EndSC { .. } => vec![
|
||||
CmdOption::new("Angle", "A"),
|
||||
CmdOption::new("chord Length", "L"),
|
||||
],
|
||||
ArcStep::CenterSE { .. } => vec![
|
||||
CmdOption::new("Angle", "A"),
|
||||
CmdOption::new("Direction", "D"),
|
||||
CmdOption::new("Radius", "R"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!matches!(
|
||||
self.step,
|
||||
ArcStep::End3 { .. }
|
||||
| ArcStep::CenterFirst
|
||||
| ArcStep::StartAfterCenter { .. }
|
||||
| ArcStep::CenterAfterStart { .. }
|
||||
| ArcStep::EndAfterStart { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
ArcStep::Start | ArcStep::Second { .. } | ArcStep::EndSC { .. } | ArcStep::CenterSE { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
ArcStep::Start => {
|
||||
self.step = ArcStep::Second { s: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ArcStep::Second { s } => {
|
||||
self.step = ArcStep::End3 { s, m: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ArcStep::End3 { s, m } => match arc_through_points(s, m, pt, self.plane) {
|
||||
Some((c, r, sa, ea)) => arc_result(c, r, sa, ea, self.plane),
|
||||
None => CmdResult::NeedPoint,
|
||||
},
|
||||
ArcStep::CenterFirst => {
|
||||
self.step = ArcStep::StartAfterCenter { c: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ArcStep::StartAfterCenter { c } => {
|
||||
self.step = ArcStep::EndSC { s: pt, c };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ArcStep::CenterAfterStart { s } => {
|
||||
self.step = ArcStep::EndSC { s, c: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ArcStep::EndSC { s, c } => self.commit(self.sc_end(s, c, pt)),
|
||||
// At a typed prompt a pick reads as AutoCAD does: the cursor's
|
||||
// direction from the centre (angle) or its distance (length,
|
||||
// radius), the cursor itself for a direction / sagitta.
|
||||
ArcStep::AngleSC { s, c } => {
|
||||
let span = self.cursor_span(s, c, pt);
|
||||
self.commit(self.sc_angle(s, c, span))
|
||||
}
|
||||
ArcStep::LengthSC { s, c } => self.commit(self.sc_length(s, c, s.distance(pt))),
|
||||
ArcStep::EndAfterStart { s } => {
|
||||
self.step = ArcStep::CenterSE { s, e: pt };
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
ArcStep::CenterSE { s, e } => self.commit(self.sc_end(s, pt, e)),
|
||||
ArcStep::AngleSE { s, e } => {
|
||||
self.commit(arc_from_sagitta(s, e, pt, self.cw, self.plane))
|
||||
}
|
||||
ArcStep::DirectionSE { s, e } => {
|
||||
self.commit(arc_continue(s, pt - s, e, self.cw, self.plane))
|
||||
}
|
||||
ArcStep::RadiusSE { s, e } => {
|
||||
self.commit(arc_from_se_radius(s, e, pt, self.cw, self.plane))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let upper = text.trim().to_uppercase();
|
||||
match self.step {
|
||||
ArcStep::Start => match upper.as_str() {
|
||||
"C" | "CE" | "CENTER" | "CENTRE" => {
|
||||
self.step = ArcStep::CenterFirst;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
ArcStep::Second { s } => match upper.as_str() {
|
||||
"C" | "CE" | "CENTER" | "CENTRE" => {
|
||||
self.step = ArcStep::CenterAfterStart { s };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"E" | "END" => {
|
||||
self.step = ArcStep::EndAfterStart { s };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
ArcStep::EndSC { s, c } => match upper.as_str() {
|
||||
"A" | "ANGLE" => {
|
||||
self.step = ArcStep::AngleSC { s, c };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"L" | "LENGTH" => {
|
||||
self.step = ArcStep::LengthSC { s, c };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
ArcStep::CenterSE { s, e } => match upper.as_str() {
|
||||
"A" | "ANGLE" => {
|
||||
self.step = ArcStep::AngleSE { s, e };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"D" | "DIRECTION" => {
|
||||
self.step = ArcStep::DirectionSE { s, e };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
"R" | "RADIUS" => {
|
||||
self.step = ArcStep::RadiusSE { s, e };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
ArcStep::AngleSC { s, c } => {
|
||||
let mut span = Self::typed_angle(text)?;
|
||||
if self.cw {
|
||||
span = -span;
|
||||
}
|
||||
Some(self.commit(self.sc_angle(s, c, span)))
|
||||
}
|
||||
ArcStep::LengthSC { s, c } => {
|
||||
let len = Self::typed_number(text)?;
|
||||
Some(self.commit(self.sc_length(s, c, len)))
|
||||
}
|
||||
ArcStep::AngleSE { s, e } => {
|
||||
let mut included = Self::typed_angle(text)?;
|
||||
if self.cw {
|
||||
included = -included;
|
||||
}
|
||||
Some(self.commit(arc_from_endpoints_angle(s, e, included, self.plane)))
|
||||
}
|
||||
ArcStep::DirectionSE { s, e } => {
|
||||
let angle = Self::typed_angle(text)?;
|
||||
let tangent = self.plane.x * angle.cos() + self.plane.y * angle.sin();
|
||||
Some(self.commit(arc_continue(s, tangent, e, self.cw, self.plane)))
|
||||
}
|
||||
ArcStep::RadiusSE { s, e } => {
|
||||
let r = Self::typed_number(text)?;
|
||||
Some(self.commit(arc_from_endpoints_radius(s, e, r, self.cw, self.plane)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
// Enter at the start prompt: continue tangent from the last
|
||||
// line or arc, as AutoCAD does.
|
||||
ArcStep::Start => CmdResult::Dispatch("ARC_CONT".into()),
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
fn enter_accepts_default_start(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn dyn_field(&self) -> crate::command::DynField {
|
||||
use crate::command::DynField;
|
||||
match self.step {
|
||||
ArcStep::AngleSC { .. } | ArcStep::AngleSE { .. } | ArcStep::DirectionSE { .. } => {
|
||||
DynField::Angle
|
||||
}
|
||||
ArcStep::LengthSC { .. } | ArcStep::RadiusSE { .. } => DynField::Distance,
|
||||
_ => DynField::Point,
|
||||
}
|
||||
}
|
||||
|
||||
fn dyn_spec(&self) -> Option<crate::command::DynSpec> {
|
||||
use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec};
|
||||
match self.step {
|
||||
ArcStep::AngleSC { s, c } => Some(DynSpec {
|
||||
anchor: DynAnchor::Point(c),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Angle)],
|
||||
guide: DynGuide::Polar,
|
||||
ref_point: Some(s),
|
||||
}),
|
||||
ArcStep::LengthSC { s, .. } => Some(DynSpec {
|
||||
anchor: DynAnchor::Point(s),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Distance)],
|
||||
guide: DynGuide::Radius,
|
||||
ref_point: None,
|
||||
}),
|
||||
ArcStep::AngleSE { s, e } => Some(DynSpec {
|
||||
anchor: DynAnchor::Point((s + e) * 0.5),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Angle)],
|
||||
guide: DynGuide::None,
|
||||
ref_point: Some(s),
|
||||
}),
|
||||
ArcStep::DirectionSE { s, .. } => Some(DynSpec {
|
||||
anchor: DynAnchor::Point(s),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Angle)],
|
||||
guide: DynGuide::Polar,
|
||||
ref_point: Some(s + self.plane.x),
|
||||
}),
|
||||
ArcStep::RadiusSE { s, .. } => Some(DynSpec {
|
||||
anchor: DynAnchor::Point(s),
|
||||
fields: vec![DynFieldSpec::new(DynRole::Radius)],
|
||||
guide: DynGuide::None,
|
||||
ref_point: None,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn dyn_commit_as_text(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
ArcStep::AngleSC { .. }
|
||||
| ArcStep::LengthSC { .. }
|
||||
| ArcStep::AngleSE { .. }
|
||||
| ArcStep::DirectionSE { .. }
|
||||
| ArcStep::RadiusSE { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn dyn_live_value(&self, cursor: DVec3) -> Option<f64> {
|
||||
match self.step {
|
||||
ArcStep::AngleSC { s, c } => Some(crate::command::dyn_display_angle_deg(
|
||||
(angle_xy(c, cursor, self.plane) - angle_xy(c, s, self.plane)) as f32,
|
||||
) as f64),
|
||||
ArcStep::LengthSC { s, .. } => Some(s.distance(cursor)),
|
||||
ArcStep::RadiusSE { s, e } => {
|
||||
arc_from_se_radius(s, e, cursor, self.cw, self.plane).map(|(_, r, _, _)| r)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
match self.step {
|
||||
ArcStep::Start | ArcStep::CenterFirst => None,
|
||||
ArcStep::Second { s } | ArcStep::EndAfterStart { s } | ArcStep::CenterAfterStart { s } => {
|
||||
Some(line_wire(s, pt))
|
||||
}
|
||||
ArcStep::StartAfterCenter { c } => Some(line_wire(c, pt)),
|
||||
ArcStep::End3 { s, m } => {
|
||||
let fallback = WireModel::solid_f64(
|
||||
"rubber_band".into(),
|
||||
vec![[s.x, s.y, s.z], [m.x, m.y, m.z], [pt.x, pt.y, pt.z]],
|
||||
WireModel::CYAN,
|
||||
false,
|
||||
);
|
||||
self.preview(arc_through_points(s, m, pt, self.plane), fallback)
|
||||
}
|
||||
ArcStep::EndSC { s, c } => self.preview(self.sc_end(s, c, pt), line_wire(c, pt)),
|
||||
ArcStep::AngleSC { s, c } => {
|
||||
let span = self.cursor_span(s, c, pt);
|
||||
self.preview(self.sc_angle(s, c, span), line_wire(c, pt))
|
||||
}
|
||||
ArcStep::LengthSC { s, c } => {
|
||||
self.preview(self.sc_length(s, c, s.distance(pt)), line_wire(s, pt))
|
||||
}
|
||||
ArcStep::CenterSE { s, e } => self.preview(self.sc_end(s, pt, e), line_wire(s, e)),
|
||||
ArcStep::AngleSE { s, e } => {
|
||||
self.preview(arc_from_sagitta(s, e, pt, self.cw, self.plane), line_wire(s, e))
|
||||
}
|
||||
ArcStep::DirectionSE { s, e } => {
|
||||
self.preview(arc_continue(s, pt - s, e, self.cw, self.plane), line_wire(s, e))
|
||||
}
|
||||
ArcStep::RadiusSE { s, e } => {
|
||||
self.preview(arc_from_se_radius(s, e, pt, self.cw, self.plane), line_wire(s, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Command 2: 3-Point (ARC_3P) ──────────────────────────────────────────
|
||||
|
||||
pub struct Arc3PCommand {
|
||||
@@ -1531,7 +1968,7 @@ impl CadCommand for ArcContCommand {
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC", "ARC_3P"] }); // Arc3PCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSA"] }); // ArcCSACommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSL"] }); // ArcCSLCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSE"] }); // ArcCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSE"] }); // ArcCSECommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCA"] }); // ArcSCACommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCE"] }); // ArcSCECommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCL"] }); // ArcSCLCommand
|
||||
@@ -1539,3 +1976,126 @@ inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SEA"] });
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SED"] }); // ArcSEDCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SER"] }); // ArcSERCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CONT"] }); // ArcContCommand
|
||||
|
||||
#[cfg(test)]
|
||||
mod acad_flow_tests {
|
||||
use super::*;
|
||||
|
||||
fn keywords(cmd: &ArcCommand) -> Vec<String> {
|
||||
cmd.options().into_iter().map(|o| o.keyword).collect()
|
||||
}
|
||||
|
||||
fn p(x: f64, y: f64) -> DVec3 {
|
||||
DVec3::new(x, y, 0.0)
|
||||
}
|
||||
|
||||
fn committed_arc(res: CmdResult) -> CadArc {
|
||||
match res {
|
||||
CmdResult::CommitAndExit(EntityType::Arc(a)) => a,
|
||||
_ => panic!("expected a committed arc"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompts_follow_autocad_keywords() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
assert_eq!(keywords(&cmd), ["C"]);
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
assert_eq!(keywords(&cmd), ["C", "E"]);
|
||||
assert!(matches!(cmd.on_text_input("C"), Some(CmdResult::NeedPoint)));
|
||||
assert!(keywords(&cmd).is_empty(), "centre pick has no keywords");
|
||||
cmd.on_point(p(5.0, 0.0));
|
||||
assert_eq!(keywords(&cmd), ["A", "L"]);
|
||||
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
cmd.on_text_input("E");
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
assert_eq!(keywords(&cmd), ["A", "D", "R"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_points_make_an_arc() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
cmd.on_point(p(5.0, 5.0));
|
||||
let arc = committed_arc(cmd.on_point(p(10.0, 0.0)));
|
||||
assert!((arc.radius - 5.0).abs() < 1e-9);
|
||||
assert!((arc.center.x - 5.0).abs() < 1e-9 && arc.center.y.abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn center_first_then_start_and_end() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_text_input("C");
|
||||
cmd.on_point(p(5.0, 0.0));
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
assert_eq!(keywords(&cmd), ["A", "L"]);
|
||||
let arc = committed_arc(cmd.on_point(p(5.0, 10.0)));
|
||||
assert!((arc.radius - 5.0).abs() < 1e-9);
|
||||
assert!(arc.start_angle.abs() < 1e-9);
|
||||
assert!((arc.end_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_center_angle_and_chord_length_typed() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
cmd.on_text_input("C");
|
||||
cmd.on_point(p(5.0, 0.0));
|
||||
cmd.on_text_input("A");
|
||||
let arc = committed_arc(cmd.on_text_input("90").expect("angle consumed"));
|
||||
assert!((arc.end_angle - arc.start_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9);
|
||||
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
cmd.on_text_input("C");
|
||||
cmd.on_point(p(5.0, 0.0));
|
||||
cmd.on_text_input("L");
|
||||
// Chord 10 on radius 5: a semicircle.
|
||||
let arc = committed_arc(cmd.on_text_input("10").expect("length consumed"));
|
||||
assert!((arc.end_angle - arc.start_angle - std::f64::consts::PI).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_end_then_radius_direction_and_angle() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
cmd.on_text_input("E");
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
cmd.on_text_input("R");
|
||||
let arc = committed_arc(cmd.on_text_input("5").expect("radius consumed"));
|
||||
assert!((arc.radius - 5.0).abs() < 1e-9);
|
||||
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
cmd.on_text_input("E");
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
cmd.on_text_input("D");
|
||||
// Tangent straight up at the start: a semicircle over the chord.
|
||||
let arc = committed_arc(cmd.on_text_input("90").expect("direction consumed"));
|
||||
assert!((arc.radius - 5.0).abs() < 1e-6);
|
||||
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
cmd.on_text_input("E");
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
cmd.on_text_input("A");
|
||||
let arc = committed_arc(cmd.on_text_input("180").expect("angle consumed"));
|
||||
assert!((arc.radius - 5.0).abs() < 1e-6);
|
||||
|
||||
// Centre pick after start / end.
|
||||
let mut cmd = ArcCommand::new();
|
||||
cmd.on_point(p(0.0, 0.0));
|
||||
cmd.on_text_input("E");
|
||||
cmd.on_point(p(10.0, 0.0));
|
||||
let arc = committed_arc(cmd.on_point(p(5.0, 0.0)));
|
||||
assert!((arc.radius - 5.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enter_at_start_continues_from_last_entity() {
|
||||
let mut cmd = ArcCommand::new();
|
||||
assert!(matches!(cmd.on_enter(), CmdResult::Dispatch(c) if c == "ARC_CONT"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user