collective

Browse / github.com/storytold/cadcraft / patch

TRIM and EXTEND: a pick in empty space starts a fence in Quick mode (fixes #1594)

[Feature Request] [CadCraft] Request for modification of the EXTEND command ### Description Request for modification of the EXTEND command to behave more like recent versions of AutoCAD that essentially start out in Fence mode (allowing multiple fence crossings without issuing additional parameters). I haven't tested TRIM much yet, but it might need the same. ### Ticket details | | | |---|---| | **App** | CadCraft | | **Type** | Feature Request | | **Reported by** | N6 (Discord username: harmonious_chipmunk_48835) — Discord user ID `1560763724212740207` | | **Submitted** | 2026-10-18 23:28:36 UTC | | **Discord server** | ArtCraft (`1359579021108842617`) | | **Submi

Status
open
Score
0.00
Adopters
0 other operators
Author
ZachAR3 (imported from pull request #2104, open)
Base
fcfdf33001cf2462bdafb6e1237becd8ef0ea1cb
Size
+45 −6 in 3 files
Review
pending
Shared
2026-10-28
Id
e555959819e62107e71943d6

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/trimextend.rs crates/engine/tests/it/trim_extend_options.rs docs/geometry-parity.md

diff --git a/crates/engine/src/cmd/trimextend.rs b/crates/engine/src/cmd/trimextend.rs--- a/crates/engine/src/cmd/trimextend.rs+++ b/crates/engine/src/cmd/trimextend.rs@@ -220,6 +220,8 @@ enum Stage {     Edges(Vec<Handle>),     /// Fence points so far.     Fence(Vec<Vec2>),+    /// Quick mode's two-point fence, started by a pick in empty space: its first point.+    QuickFence(Vec2),     /// A crossing window: its first corner once picked.     Crossing(Option<Vec2>),     Mode,@@ -356,6 +358,7 @@ impl Interactive for TrimM {                 None => Prompt::new("Specify first fence point", Accept::POINT),                 Some(l) => Prompt::new("Specify next fence point", Accept::POINT).kw(&["Undo"]).base(*l),             },+            Stage::QuickFence(a) => Prompt::new("Specify next fence point", Accept::POINT).base(*a),             Stage::Crossing(None) => Prompt::new("Specify first corner", Accept::POINT),             Stage::Crossing(Some(_)) => Prompt::new("Specify opposite corner", Accept::POINT),             Stage::Mode => {@@ -455,6 +458,16 @@ impl Interactive for TrimM {                 }                 Ok(Step::Continue)             }+            Stage::QuickFence(a) => {+                self.stage = Stage::Pick;+                if let Input::Point(b) = i {+                    let r = self.step(s, |m, s| fence(s, &[a, b], extending, m.edges(quick, implied), quick).map(|_| ()));+                    if let Err(e) = r {+                        s.echo(e.to_string());+                    }+                }+                Ok(Step::Continue)+            }             Stage::Crossing(first) => {                 match (first, i) {                     (None, Input::Point(p)) => self.stage = Stage::Crossing(Some(p)),@@ -475,8 +488,9 @@ impl Interactive for TrimM {                     let ap = s.pixel_size() * s.settings.pickbox.max(1.0) * 1.5;                     let space = s.space();                     let Some(h) = crate::select::pick(s.doc()?, &space, p, ap) else {-                        // A pick in empty space opens a crossing window there.-                        self.stage = Stage::Crossing(Some(p));+                        // A pick in empty space starts a two-point fence in Quick mode (as in+                        // AutoCAD, #1594) and a crossing window in Standard mode.+                        self.stage = if quick { Stage::QuickFence(p) } else { Stage::Crossing(Some(p)) };                         return Ok(Step::Continue);                     };                     let r = self.step(s, |m, s| apply(s, h, p, extending, m.edges(quick, implied), quick).map(|_| ()));@@ -525,6 +539,7 @@ impl Interactive for TrimM {                 .windows(2)                 .filter_map(|w| Some(line(*w.first()?, *w.get(1)?)))                 .collect(),+            Stage::QuickFence(a) => vec![line(*a, c)],             Stage::Crossing(Some(a)) => vec![lwpoly(rect_vertices(*a, c), true)],             _ => Vec::new(),         }diff --git a/crates/engine/tests/it/trim_extend_options.rs b/crates/engine/tests/it/trim_extend_options.rs--- a/crates/engine/tests/it/trim_extend_options.rs+++ b/crates/engine/tests/it/trim_extend_options.rs@@ -112,10 +112,33 @@ fn crossing_window_trims_what_it_touches() {     s.script("TRIM C 4,-1 6,1\n\n").unwrap();     assert!(s.running.is_none(), "{}", s.prompt_text());     assert_eq!(lines(&s), with_edges(&[((-5.0, 0.0), (0.0, 0.0)), ((-5.0, 2.0), (15.0, 2.0)), ((10.0, 0.0), (15.0, 0.0))]));-    // A pick in empty space opens a crossing window too.+}++#[test]+fn a_pick_in_empty_space_starts_a_fence_in_quick_mode_and_a_window_in_standard_mode() {+    // #1594: Quick mode's two-point fence trims what it crosses and returns to the first prompt.+    for cmd in ["TRIM", "EXTEND"] {+        let mut s = ladder();+        s.cmdline(cmd).unwrap();+        s.cmdline("4,-4").unwrap();+        assert!(s.prompt_text().contains("Specify next fence point"), "{cmd}: {}", s.prompt_text());+    }     let mut s = ladder();-    s.script("TRIM 4,-4 6,1\n\n").unwrap();+    s.script("TRIM 4,-4 6,1\n").unwrap();+    assert!(s.prompt_text().contains("Select object to trim"), "{}", s.prompt_text());+    s.cmdline("").unwrap();     assert_eq!(lines(&s), with_edges(&[((-5.0, 0.0), (0.0, 0.0)), ((-5.0, 2.0), (15.0, 2.0)), ((10.0, 0.0), (15.0, 0.0))]));+    // EXTEND's fence extends both lines it crosses to the boundary at x = 10.+    let mut s = session(&["10,-5 10,5", "0,0 4,0", "0,2 4,2"]);+    s.script("EXTEND 3,-1 3,3\n").unwrap();+    assert!(s.prompt_text().contains("Select object to extend"), "{}", s.prompt_text());+    s.cmdline("").unwrap();+    assert!(s.running.is_none(), "{}", s.prompt_text());+    assert_eq!(lines(&s), vec![((0.0, 0.0), (10.0, 0.0)), ((0.0, 2.0), (10.0, 2.0)), ((10.0, -5.0), (10.0, 5.0))]);+    // Standard mode keeps the crossing window.+    let mut s = ladder();+    s.script("TRIM O S\n\n4,-4\n").unwrap();+    assert!(s.prompt_text().contains("Specify opposite corner"), "{}", s.prompt_text()); }  #[test]diff --git a/docs/geometry-parity.md b/docs/geometry-parity.md--- a/docs/geometry-parity.md+++ b/docs/geometry-parity.md@@ -1,6 +1,6 @@ # Geometry parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (bounded constraint expression and parameter depth) · **Target:** Autodesk AutoCAD 2027+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (TRIM/EXTEND Quick mode two-point fence) · **Target:** Autodesk AutoCAD 2027  CAD users trust numbers: a trimmed endpoint, an offset, an intersection or an area is either right to the last displayed digit or the tool is unusable for production drawings. This document@@ -80,7 +80,7 @@ constraints 10%.  | Command | AutoCAD supports | CADCraft supports | |---|---|---|-| TRIM | every curve type, quick and standard modes | line, arc, circle, polyline; ellipse and spline via sampled cutting (`modify.rs:485-656`, `modify2.rs` `trim_other`); a closed spline leaves one open spline On a closed polyline a pick before the first or after the last cut keeps the run between the cuts and nothing more (#726). |+| TRIM | every curve type, quick and standard modes | line, arc, circle, polyline; ellipse and spline via sampled cutting (`modify.rs:485-656`, `modify2.rs` `trim_other`); a closed spline leaves one open spline On a closed polyline a pick before the first or after the last cut keeps the run between the cuts and nothing more (#726). In Quick mode TRIM and EXTEND start a two-point fence from a pick in empty space, Standard mode a crossing window (#1594). | | EXTEND | every open curve | line, arc, polyline, elliptical arc, open spline (runs on along its end tangent, `modify2/curve_edits.rs`) | | FILLET | lines, arcs, circles, polylines, ellipses, splines, xlines, rays; Trim/No trim | lines, arcs, circles (TTR circle), polyline mode, Trim/No trim, Multiple; elliptical arcs and splines with any of these (tangent circle from sampled offsets refined by Newton's method, `geom/src/fillet.rs`; arcs trimmed or extended round the ellipse, splines split at the touch point); two straight segments of one polyline (adjacent, or with one between that the corner replaces), a polyline's straight end segment with a line (the line joins it); not polyline arc segments, xlines or rays | | CHAMFER | lines, polylines, xlines, rays | lines, Distance/Angle, whole polylines (Polyline option), polyline segments as for FILLET (`modify2/fillet_pline.rs`); refuses arcs, ellipses and splines with a message, as AutoCAD; not xlines or rays |@@ -157,6 +157,7 @@ Total ≈ 90–140 h; items 1–6 parallelise across two or three agents.  | Date | Change | Summary | |---|---|---|+| 2026-10-25 | minor | TRIM and EXTEND in Quick mode: a pick in empty space starts a two-point fence, as in AutoCAD; Standard mode keeps the crossing window (#1594) | | 2026-10-22 | minor | Constraint expressions count operator chains toward the syntax-tree depth limit; evaluation shares one depth limit across operators and parameter references, with a 1 MiB-stack regression (#1936) | | 2026-10-14 | trivial | JOIN Close: a single arc becomes its circle, a single elliptical arc its full ellipse (#1175) | | 2026-10-12 | trivial | TRIM on a closed polyline keeps only the complementary run when the pick lies past the last cut (#726) |