diff --git a/assets/icons/arc/arc_cont.svg b/assets/icons/arc/arc_cont.svg
new file mode 100644
index 000000000..c9919dde3
--- /dev/null
+++ b/assets/icons/arc/arc_cont.svg
@@ -0,0 +1,7 @@
+
diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs
index 45fec88b0..acd779976 100644
--- a/src/app/command_driver.rs
+++ b/src/app/command_driver.rs
@@ -11,10 +11,12 @@ impl OpenCADStudio {
/// lives in exactly one place. No-op when no command is active.
pub(super) fn feed_command(&mut self, input: StepInput) -> Task {
let i = self.active_tab;
+ let ctrl = self.ctrl_down;
let result: Option = {
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
return Task::none();
};
+ cmd.set_ctrl(ctrl);
match input {
StepInput::Point(p) => Some(cmd.on_point(p)),
StepInput::Text(s) => cmd.on_text_input(&s),
@@ -284,6 +286,9 @@ impl OpenCADStudio {
&path,
);
}
+ // Record where this draw ended so ARC_CONT can continue from it
+ // (before `entity` is moved into commit_entity).
+ self.update_cont_anchor(&entity);
let label = self.history_label_from_active_cmd(i, "ENTITY");
self.push_undo_snapshot(i, label);
self.commit_entity(entity);
diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs
index efddf7465..30f1d2035 100644
--- a/src/app/commands/draw.rs
+++ b/src/app/commands/draw.rs
@@ -292,6 +292,31 @@ impl OpenCADStudio {
self.command_line.push_info(&new_cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
+ "ARC_CONT" => {
+ use crate::modules::draw::draw::arc::{continue_anchor, ArcContCommand};
+ // Prefer the anchor recorded when the last line/arc was drawn (it
+ // knows the true drawing-end tangent); otherwise fall back to the
+ // last line/arc found in the document (e.g. after a file load).
+ let seed = self.cont_anchor.or_else(|| {
+ self.tabs[i]
+ .scene
+ .document
+ .entities()
+ .filter_map(|e| continue_anchor(e, None))
+ .last()
+ });
+ match seed {
+ Some((s, tangent)) => {
+ let new_cmd = ArcContCommand::new(s, tangent);
+ self.command_line.push_info(&new_cmd.prompt());
+ self.tabs[i].active_cmd = Some(Box::new(new_cmd));
+ }
+ None => {
+ self.command_line
+ .push_info("ARC Continue No previous line or arc to continue.");
+ }
+ }
+ }
"RECT" | "RECTANG" | "REC" => {
use crate::modules::draw::draw::shapes::RectCommand;
diff --git a/src/app/mod.rs b/src/app/mod.rs
index 69ec00905..e742d3637 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -392,6 +392,10 @@ pub(super) struct OpenCADStudio {
show_layout_tabs: bool,
/// Last point committed by a drawing command — used as ortho/polar base.
last_point: Option,
+ /// Endpoint + unit exit-tangent of the most recently drawn line/arc, so
+ /// `ARC_CONT` (Arc → Continue) can start tangentially from where drawing
+ /// ended. `None` once a non-line/arc entity is committed.
+ cont_anchor: Option<(glam::DVec3, glam::DVec3)>,
/// OS window Id for the floating Layer Properties Manager (None when closed).
/// OS window Id of the primary application window.
main_window: Option,
@@ -2150,6 +2154,7 @@ impl OpenCADStudio {
clipboard_deps: ClipboardDeps::default(),
shift_down: false,
ctrl_down: false,
+ cont_anchor: None,
mtext_editor: None,
text_inline: None,
layout_context_menu: None,
diff --git a/src/app/update/dynamic.rs b/src/app/update/dynamic.rs
index f6b76b849..abc157aed 100644
--- a/src/app/update/dynamic.rs
+++ b/src/app/update/dynamic.rs
@@ -198,6 +198,26 @@ impl OpenCADStudio {
}
}
+ /// Push the live Ctrl state into tab `i`'s active command before it builds a
+ /// preview/commit (arc-direction flip on `ARC_CONT`, etc.).
+ pub(in crate::app) fn push_ctrl_to_cmd(&mut self, i: usize) {
+ let ctrl = self.ctrl_down;
+ if let Some(c) = self.tabs[i].active_cmd.as_mut() {
+ c.set_ctrl(ctrl);
+ }
+ }
+
+ /// Cache the endpoint + exit tangent of a just-committed line/arc so a
+ /// following `ARC_CONT` starts tangentially from where drawing ended. Set to
+ /// `None` for any other entity kind. Uses `last_point` (the final pick) to
+ /// pick the endpoint the pen actually finished on.
+ pub(in crate::app) fn update_cont_anchor(&mut self, entity: &acadrust::EntityType) {
+ let last = self
+ .last_point
+ .map(|v| glam::DVec3::new(v.x as f64, v.y as f64, v.z as f64));
+ self.cont_anchor = crate::modules::draw::draw::arc::continue_anchor(entity, last);
+ }
+
pub(in crate::app) fn dyn_resolve_point(&self) -> Option {
use crate::app::document::DynComponent;
let i = self.active_tab;
diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs
index 47e42f885..87cc226ea 100644
--- a/src/app/update/mod.rs
+++ b/src/app/update/mod.rs
@@ -1751,8 +1751,17 @@ impl OpenCADStudio {
}
Message::SetModifiers { shift, ctrl } => {
+ let ctrl_changed = self.ctrl_down != ctrl;
self.shift_down = shift;
self.ctrl_down = ctrl;
+ // A live command may key its preview off Ctrl (arc-direction
+ // flip). Rebuild it at the current cursor so the flip shows
+ // without waiting for the next mouse move.
+ let i = self.active_tab;
+ if ctrl_changed && self.tabs[i].active_cmd.is_some() {
+ let p = self.tabs[i].last_cursor_screen;
+ return Task::done(Message::ViewportMove(p));
+ }
Task::none()
}
diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs
index 5f0714d07..98b2f4aca 100644
--- a/src/app/update/viewport.rs
+++ b/src/app/update/viewport.rs
@@ -1090,9 +1090,11 @@ pub(super) fn on_tick(&mut self, t: Instant) -> Task {
s.screen.y += tile_b.y;
}
- // Give the command the current UCS before it builds its
- // rubber-band preview (and, by persistence, its commit).
+ // Give the command the current UCS + Ctrl state before it
+ // builds its rubber-band preview (and, by persistence, its
+ // commit).
self.push_ucs_to_cmd(i);
+ self.push_ctrl_to_cmd(i);
let mut previews = if needs_structure {
let mut p = self.tabs[i]
.active_cmd
diff --git a/src/command.rs b/src/command.rs
index b2c6cd001..27629f385 100644
--- a/src/command.rs
+++ b/src/command.rs
@@ -552,6 +552,11 @@ pub trait CadCommand: Send {
/// ignore it. Called before each point / preview dispatch.
fn set_ucs(&mut self, _ucs: glam::Mat4) {}
+ /// Push the live Ctrl-key state into the command before each preview/commit
+ /// dispatch. Commands that offer a Ctrl toggle (e.g. arc-direction flip on
+ /// `ARC_CONT`) store it; most commands ignore it. Default no-op.
+ fn set_ctrl(&mut self, _ctrl: bool) {}
+
/// Called when the user left-clicks in the viewport (point pick).
fn on_point(&mut self, pt: DVec3) -> CmdResult;
diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs
index d4fa47795..fa4a4c86f 100644
--- a/src/modules/draw/draw/arc.rs
+++ b/src/modules/draw/draw/arc.rs
@@ -1,16 +1,17 @@
// Arc tool — ribbon dropdown + all OpenCADStudio arc creation methods.
//
-// Methods:
-// ARC — Center, Start, End (CSE — default)
-// ARC_3P — 3-Point
+// Methods (dropdown order):
+// ARC_3P — 3-Point (default)
// ARC_SCE — Start, Center, End
// ARC_SCA — Start, Center, Angle
// ARC_SCL — Start, Center, Length of chord
-// ARC_SEA — Start, End, Angle (sagitta-based pick)
-// ARC_SER — Start, End, Radius (radius defined by dist cursor→start)
-// ARC_SED — Start, End, Direction (tangent at start)
+// ARC_SEA — Start, End, Angle (sagitta-based pick)
+// ARC_SED — Start, End, Direction (tangent at start)
+// ARC_SER — Start, End, Radius (radius defined by dist cursor→start)
+// ARC — Center, Start, End (CSE)
// ARC_CSA — Center, Start, Angle
// ARC_CSL — Center, Start, Length of chord
+// ARC_CONT— Continue (tangent to previous line/arc; pick end only)
use acadrust::types::Vector3;
use acadrust::{Arc as CadArc, EntityType};
@@ -43,26 +44,29 @@ const ICON_CSA: IconKind =
IconKind::Svg(include_bytes!("../../../../assets/icons/arc/arc_csa.svg"));
const ICON_CSL: IconKind =
IconKind::Svg(include_bytes!("../../../../assets/icons/arc/arc_csl.svg"));
+const ICON_CONT: IconKind =
+ IconKind::Svg(include_bytes!("../../../../assets/icons/arc/arc_cont.svg"));
// ── Dropdown metadata ──────────────────────────────────────────────────────
pub const DROPDOWN_ID: &str = "ARC";
pub const DROPDOWN_ITEMS: &[(&str, &str, IconKind)] = &[
- ("ARC", "Center, Start, End", ICON_CSE),
("ARC_3P", "3-Point", ICON_3P),
("ARC_SCE", "Start, Center, End", ICON_SCE),
("ARC_SCA", "Start, Center, Angle", ICON_SCA),
("ARC_SCL", "Start, Center, Length", ICON_SCL),
("ARC_SEA", "Start, End, Angle", ICON_SEA),
- ("ARC_SER", "Start, End, Radius", ICON_SER),
("ARC_SED", "Start, End, Direction", ICON_SED),
+ ("ARC_SER", "Start, End, Radius", ICON_SER),
+ ("ARC", "Center, Start, End", ICON_CSE),
("ARC_CSA", "Center, Start, Angle", ICON_CSA),
("ARC_CSL", "Center, Start, Length", ICON_CSL),
+ ("ARC_CONT", "Continue", ICON_CONT),
];
-/// Default icon — falls back to CSE before first use.
-pub const ICON: IconKind = ICON_CSE;
+/// Default icon — falls back to 3-Point before first use.
+pub const ICON: IconKind = ICON_3P;
// ── Shared math helpers ────────────────────────────────────────────────────
@@ -209,6 +213,68 @@ fn arc_from_direction(s: DVec3, e: DVec3, dir_pt: DVec3) -> Option<(DVec3, f64)>
Some((center, center.distance(s)))
}
+/// Tangent-continuing arc: starts at `s` leaving in unit direction `t` (forward,
+/// G1-continuous with the previous entity) and passes through `e`. Returns
+/// `(center, radius, start_angle, end_angle)` with the CCW sweep oriented so the
+/// drawn arc actually leaves `s` along `+t` (not the reflex complement).
+///
+/// `arc_from_direction` treats the tangent as an undirected line (±t give the
+/// same circle), so on its own the CCW-only renderer would draw the wrong arc
+/// whenever `e` lies to the right of `+t`. We fix the sweep direction here by
+/// checking the circle's CCW travel direction at `s` and swapping the angles
+/// when it opposes `+t`. `flip` (Ctrl) selects the complementary arc — the other
+/// way around the same circle.
+fn arc_continue(s: DVec3, t: DVec3, e: DVec3, flip: bool) -> Option<(DVec3, f64, f64, f64)> {
+ let (center, radius) = arc_from_direction(s, e, s + t)?;
+ // CCW travel direction on this circle at s = (s - center) rotated +90°.
+ let rs = s - center;
+ let travel_s = DVec3::new(-rs.y, rs.x, 0.0);
+ let forward = travel_s.dot(t) >= 0.0;
+ // Draw CCW s→e when the CCW-from-s arc leaves along +t; else CCW e→s. `flip`
+ // inverts the choice to draw the complementary arc.
+ let (sa, ea) = if forward ^ flip {
+ (angle_xy(center, s), angle_xy(center, e))
+ } else {
+ (angle_xy(center, e), angle_xy(center, s))
+ };
+ Some((center, radius, sa, ea))
+}
+
+/// Continuation anchor for `ARC_CONT`: the point where drawing a line/arc ended
+/// plus the unit tangent leaving it, used to start a tangent-continuing arc.
+/// `last` (the final pick point) disambiguates which endpoint the drawing ended
+/// on — essential because a committed arc stores geometric CCW start/end angles,
+/// not the direction the pen travelled. Returns `None` for other entity kinds.
+pub fn continue_anchor(entity: &EntityType, last: Option) -> Option<(DVec3, DVec3)> {
+ let nearer_first = |a: DVec3, b: DVec3| match last {
+ Some(p) => p.distance(a) <= p.distance(b),
+ None => true, // default to the entity's stored end
+ };
+ match entity {
+ EntityType::Line(l) => {
+ let a = DVec3::new(l.start.x, l.start.y, l.start.z);
+ let b = DVec3::new(l.end.x, l.end.y, l.end.z);
+ let (p_end, p_start) = if nearer_first(b, a) { (b, a) } else { (a, b) };
+ let t = (p_end - p_start).normalize_or_zero();
+ (t.length_squared() > 1e-12).then_some((p_end, t))
+ }
+ EntityType::Arc(a) => {
+ let sp_v = a.start_point();
+ let ep_v = a.end_point();
+ let sp = DVec3::new(sp_v.x, sp_v.y, sp_v.z);
+ let ep = DVec3::new(ep_v.x, ep_v.y, ep_v.z);
+ if nearer_first(ep, sp) {
+ // Ended at end_angle: outward tangent = CCW travel there.
+ Some((ep, DVec3::new(-a.end_angle.sin(), a.end_angle.cos(), 0.0)))
+ } else {
+ // Ended at start_angle: outward tangent = CW travel there.
+ Some((sp, DVec3::new(a.start_angle.sin(), -a.start_angle.cos(), 0.0)))
+ }
+ }
+ _ => None,
+ }
+}
+
/// Compute end_angle from a chord-length pick (SCL / CSL semantics).
/// `chord_len` is clamped to [0, 2r].
fn end_angle_from_chord_len(start_angle: f64, chord: f64, r: f64) -> f64 {
@@ -1258,6 +1324,60 @@ impl CadCommand for ArcCSLCommand {
}
+// ── Command 11: Continue (ARC_CONT) ──────────────────────────────────────
+// Tangent-continuing arc from the previous line/arc endpoint. The start point
+// and start tangent are seeded at dispatch (from `continue_anchor`); the user
+// picks only the end point, so the arc joins the previous entity smoothly.
+
+pub struct ArcContCommand {
+ s: DVec3,
+ tangent: DVec3,
+ /// Live Ctrl state (set via `set_ctrl`): flips the arc to the other way.
+ ctrl: bool,
+}
+
+impl ArcContCommand {
+ pub fn new(s: DVec3, tangent: DVec3) -> Self {
+ Self {
+ s,
+ tangent,
+ ctrl: false,
+ }
+ }
+}
+
+impl CadCommand for ArcContCommand {
+ fn name(&self) -> &'static str {
+ "ARC_CONT"
+ }
+ fn prompt(&self) -> String {
+ "ARC Continue Specify end point [Ctrl = flip direction]:".into()
+ }
+ fn set_ctrl(&mut self, ctrl: bool) {
+ self.ctrl = ctrl;
+ }
+ fn on_point(&mut self, pt: DVec3) -> CmdResult {
+ match arc_continue(self.s, self.tangent, pt, self.ctrl) {
+ Some((center, radius, sa, ea)) => {
+ CmdResult::CommitAndExit(make_arc(center, radius, sa, ea))
+ }
+ None => CmdResult::Cancel,
+ }
+ }
+ fn on_enter(&mut self) -> CmdResult {
+ CmdResult::Cancel
+ }
+ fn on_escape(&mut self) -> CmdResult {
+ CmdResult::Cancel
+ }
+ fn on_mouse_move(&mut self, pt: DVec3) -> Option {
+ match arc_continue(self.s, self.tangent, pt, self.ctrl) {
+ Some((center, radius, sa, ea)) => Some(arc_preview(center, radius, sa, ea)),
+ None => Some(line_wire(self.s, pt)),
+ }
+ }
+}
+
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_3P"] }); // Arc3PCommand
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSA"] }); // ArcCSACommand
@@ -1269,3 +1389,4 @@ inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCL"] });
inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SEA"] }); // ArcSEACommand
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
diff --git a/src/modules/draw/mod.rs b/src/modules/draw/mod.rs
index ac419385a..4d658ff6d 100644
--- a/src/modules/draw/mod.rs
+++ b/src/modules/draw/mod.rs
@@ -62,7 +62,7 @@ impl CadModule for DrawModule {
label: "Arc",
icon: arc::ICON,
items: arc::DROPDOWN_ITEMS.to_vec(),
- default: "ARC",
+ default: "ARC_3P",
},
RibbonItem::Dropdown {
id: shapes::DROPDOWN_ID,