collective

Browse / github.com/storytold/cadcraft / patch

Drafting Settings: tabs, and a Dynamic Input tab with pointer and dimension input apart

Dynmode Dynmode doesnt do what it's supposed to do, respectively move the command input text box from the one on the bottom to a floating one next to your cursor. Also when you write a command, you have to press Tab to confirm, instead of just pressing enter on the first suggestion. <img width="403" height="283" alt="Image" src="https://github.com/user-attachments/assets/279afa26-403f-402d-905e-75ae4235ec23" /> This is what it should look like <img width="471" height="458" alt="Image" src="https://github.com/user-attachments/assets/1288aa67-cf31-4ac4-bc53-f336c1b14c20" /> This is the dynamic inp

Status
open
Score
0.00
Adopters
0 other operators
Author
thealexspike (imported from pull request #2136, open)
Base
e86d895ba1086a7efc2a83048348f89098282d08
Size
+581 −113 in 14 files
Review
pending
Shared
2026-10-28
Id
522f56802620e3f35007537a

How it went

No other operator has reported using it yet.

Diff

raw · crates/engine/src/cmd/options.rs crates/engine/src/cmd/settings.rs crates/engine/src/grip_dims.rs crates/engine/src/lib.rs crates/engine/src/sysvars.rs crates/ui-egui/src/canvas.rs crates/ui-egui/src/cmdline.rs crates/ui-egui/src/context_menu.rs crates/ui-egui/src/dialogs.rs crates/ui-egui/src/grip_dyn.rs crates/ui-egui/tests/it/drafting_dynamic_input.rs crates/ui-egui/tests/it/drafting_polar_increment.rs crates/ui-egui/tests/it/main.rs docs/ui-parity.md

diff --git a/crates/engine/src/cmd/options.rs b/crates/engine/src/cmd/options.rs--- a/crates/engine/src/cmd/options.rs+++ b/crates/engine/src/cmd/options.rs@@ -27,6 +27,7 @@ pub const PROFILE_VARS: &[&str] = &[     "DYNMODE",     "DYNPIFORMAT",     "DYNPICOORDS",+    "DYNPIVIS",     "DYNPROMPT",     "DYNDIGRIP",     "DYNDIVIS",@@ -107,9 +108,13 @@ pub fn apply(s: &mut Session, values: &Value) -> Result<()> { /// skipped (an old or hand-edited profile never stops the app from starting). pub fn restore(s: &mut Session, saved: &Value) {     let Some(obj) = saved.as_object() else { return };+    // Profiles saved before DYNMODE took its bits (no DYNPIVIS yet) stored 1 for "on": both inputs.+    let old_dynmode = !obj.contains_key("DYNPIVIS");+    let both = json!(3);     for (name, v) in obj.iter().take(MAX_ENTRIES) {         if is_profile_var(name) {             let before = s.settings.clone();+            let v = if old_dynmode && name.eq_ignore_ascii_case("DYNMODE") && v.as_i64() == Some(1) { &both } else { v };             if crate::sysvars::set(s, name, v).is_err() {                 s.settings = before;             }@@ -132,6 +137,8 @@ pub fn reset(s: &mut Session) {     st.gridmode = d.gridmode;     st.gridmajor = d.gridmajor;     st.dynmode = d.dynmode;+    st.dyninput = d.dyninput;+    st.dynpivis = d.dynpivis;     st.dynpi_cartesian = d.dynpi_cartesian;     st.dynpi_absolute = d.dynpi_absolute;     st.osnaphatch = d.osnaphatch;@@ -191,4 +198,41 @@ mod tests {         restore(&mut b, &saved);         assert_eq!(profile(&b), saved);     }++    #[test]+    fn dynmode_keeps_pointer_and_dimension_input_apart() {+        let mut s = Session::new();+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(3)));+        apply(&mut s, &json!({"DYNMODE": 2})).unwrap();+        assert!(!s.settings.dyn_pointer() && s.settings.dyn_dimension());+        // F12 turns it off and back on to the same inputs; DYNMODE reads negative meanwhile.+        s.execute("dynmode", &Value::Null).unwrap();+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(-2)));+        assert!(!s.settings.dyn_dimension());+        s.execute("dynmode", &Value::Null).unwrap();+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(2)));+        // Both off: F12 turns both on.+        apply(&mut s, &json!({"DYNMODE": 0})).unwrap();+        s.execute("dynmode", &Value::Null).unwrap();+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(3)));+        // `false`/`true` switch it like F12.+        apply(&mut s, &json!({"DYNMODE": 1})).unwrap();+        apply(&mut s, &json!({"DYNMODE": false})).unwrap();+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(-1)));+        apply(&mut s, &json!({"DYNMODE": true})).unwrap();+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(1)));+        assert!(apply(&mut s, &json!({"DYNMODE": 4})).is_err());+        assert!(apply(&mut s, &json!({"DYNPIVIS": 3})).is_err());+    }++    #[test]+    fn a_profile_saved_before_dynmode_bits_keeps_both_inputs() {+        // Saved when DYNMODE was on/off: 1 meant on.+        let mut s = Session::new();+        restore(&mut s, &json!({"DYNMODE": 1}));+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(3)));+        // Saved with DYNPIVIS: 1 is pointer input only.+        restore(&mut s, &json!({"DYNMODE": 1, "DYNPIVIS": 1}));+        assert_eq!(crate::sysvars::get(&s, "DYNMODE"), Some(json!(1)));+    } }diff --git a/crates/engine/src/cmd/settings.rs b/crates/engine/src/cmd/settings.rs--- a/crates/engine/src/cmd/settings.rs+++ b/crates/engine/src/cmd/settings.rs@@ -47,12 +47,19 @@ pub fn specs() -> Vec<CommandSpec> {             .enabled(always)             .noundo()             .transparent(),-        CommandSpec::new("dynmode", "Dynamic Input", |s, p| toggle(s, p, |st| &mut st.dynmode, "Dynamic Input"))-            .key("F12")-            .params("{on?: bool}")-            .enabled(always)-            .noundo()-            .transparent(),+        CommandSpec::new("dynmode", "Dynamic Input", |s, p| {+            let r = toggle(s, p, |st| &mut st.dynmode, "Dynamic Input");+            // DYNMODE 0 (both inputs off): F12 turns both back on.+            if s.settings.dynmode && s.settings.dyninput == 0 {+                s.settings.dyninput = 3;+            }+            r+        })+        .key("F12")+        .params("{on?: bool}")+        .enabled(always)+        .noundo()+        .transparent(),         CommandSpec::new("lwdisplay", "Show/Hide Lineweight", |s, p| toggle(s, p, |st| &mut st.lwdisplay, "Lineweight"))             .params("{on?: bool}")             .enabled(always)diff --git a/crates/engine/src/grip_dims.rs b/crates/engine/src/grip_dims.rs--- a/crates/engine/src/grip_dims.rs+++ b/crates/engine/src/grip_dims.rs@@ -178,10 +178,10 @@ impl Locus { }  /// The dimensions to show for the grip the current prompt stretches, in cycle order (Tab), as-/// DYNDIVIS and DYNDIGRIP ask; empty when no single grip is being stretched or Dynamic Input+/// DYNDIVIS and DYNDIGRIP ask; empty when no single grip is being stretched or dimension input /// is off. pub fn grip_dims(s: &Session) -> Vec<GripDim> {-    if !s.settings.dynmode {+    if !s.settings.dyn_dimension() {         return Vec::new();     }     let Some((h, i)) = s.current_prompt().and_then(|p| p.grip) else { return Vec::new() };diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs--- a/crates/engine/src/lib.rs+++ b/crates/engine/src/lib.rs@@ -364,7 +364,14 @@ pub struct Settings {     pub measureinit: bool,     /// -VIEW sEttings > Layer snapshot: named views save the layer settings with them.     pub view_layer_snapshot: bool,+    /// Dynamic Input on (F12, the sign of DYNMODE).     pub dynmode: bool,+    /// What Dynamic Input shows (DYNMODE's magnitude, bits): 1 pointer input, 2 dimension input;+    /// 3 (default) both.+    pub dyninput: u8,+    /// DYNPIVIS: when pointer input shows: 0 once coordinates are typed, 1 (default) when a command+    /// asks for a point, 2 always, even with no command running.+    pub dynpivis: u8,     /// DYNDIGRIP: which dimensions grip dimensional input shows while a grip is stretched (bits:     /// 1 resulting length, 2 length change, 4 absolute angle, 8 angle change, 16 radius); read     /// when DYNDIVIS = 2.@@ -483,6 +490,8 @@ impl Default for Settings {             measureinit: false,             view_layer_snapshot: true,             dynmode: true,+            dyninput: 3,+            dynpivis: 1,             dyndigrip: 31,             dyndivis: 1,             dynpi_cartesian: false,@@ -532,6 +541,19 @@ impl Default for Settings {     } } +impl Settings {+    /// Dynamic Input shows pointer input: X,Y boxes for a first point, and the boxes for next+    /// points in the DYNPIFORMAT/DYNPICOORDS format.+    pub fn dyn_pointer(&self) -> bool {+        self.dynmode && self.dyninput & 1 != 0+    }+    /// Dynamic Input shows dimension input: distance and angle from the last point, grip+    /// dimensions (DYNDIGRIP, DYNDIVIS) and a distance prompt's length field.+    pub fn dyn_dimension(&self) -> bool {+        self.dynmode && self.dyninput & 2 != 0+    }+}+ /// An interactive command in progress. pub struct Running {     pub id: String,diff --git a/crates/engine/src/sysvars.rs b/crates/engine/src/sysvars.rs--- a/crates/engine/src/sysvars.rs+++ b/crates/engine/src/sysvars.rs@@ -33,6 +33,7 @@ const SESSION_VARS: &[&str] = &[     "DYNMODE",     "DYNPIFORMAT",     "DYNPICOORDS",+    "DYNPIVIS",     "DYNPROMPT",     "LWDISPLAY",     "PLOTTRANSPARENCYOVERRIDE",@@ -102,7 +103,12 @@ pub fn get(s: &Session, name: &str) -> Option<Value> {         "GRIDMAJOR" => json!(st.gridmajor),         "DYNDIGRIP" => json!(st.dyndigrip),         "DYNDIVIS" => json!(st.dyndivis),-        "DYNMODE" => json!(i32::from(st.dynmode)),+        // 1 pointer input, 2 dimension input, 3 both; negative while F12 has turned it off.+        "DYNMODE" => {+            let d = i32::from(st.dyninput);+            json!(if st.dynmode { d } else { -d })+        }+        "DYNPIVIS" => json!(st.dynpivis),         "DYNPIFORMAT" => json!(i32::from(st.dynpi_cartesian)),         "DYNPICOORDS" => json!(i32::from(st.dynpi_absolute)),         "DYNPROMPT" => json!(i32::from(st.dynprompt)),@@ -313,7 +319,19 @@ pub fn set(s: &mut Session, name: &str, v: &Value) -> Result<()> {         "GRIDMAJOR" => st.gridmajor = as_f64(v).ok_or_else(bad)?.clamp(1.0, 100.0) as u32,         "DYNDIGRIP" => st.dyndigrip = as_i(v).filter(|i| (0..=31).contains(i)).and_then(|i| u32::try_from(i).ok()).ok_or_else(bad)?,         "DYNDIVIS" => st.dyndivis = as_i(v).filter(|i| (0..=2).contains(i)).and_then(|i| u8::try_from(i).ok()).ok_or_else(bad)?,-        "DYNMODE" => st.dynmode = as_bool(v).ok_or_else(bad)?,+        // `true`/`false` turn Dynamic Input on and off as F12 does, keeping what it shows.+        "DYNMODE" if v.is_boolean() => {+            st.dynmode = v.as_bool() == Some(true);+            if st.dynmode && st.dyninput == 0 {+                st.dyninput = 3;+            }+        }+        "DYNMODE" => {+            let i = as_i(v).filter(|i| (-3..=3).contains(i)).ok_or_else(bad)?;+            st.dynmode = i > 0;+            st.dyninput = u8::try_from(i.unsigned_abs()).map_err(|_| bad())?;+        }+        "DYNPIVIS" => st.dynpivis = small(v, 2).ok_or_else(bad)?,         "DYNPIFORMAT" => st.dynpi_cartesian = as_bool(v).ok_or_else(bad)?,         "DYNPICOORDS" => st.dynpi_absolute = as_bool(v).ok_or_else(bad)?,         "DYNPROMPT" => st.dynprompt = as_bool(v).ok_or_else(bad)?,diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs--- a/crates/ui-egui/src/canvas.rs+++ b/crates/ui-egui/src/canvas.rs@@ -2349,12 +2349,33 @@ fn dynamic_input(app: &mut CadApp, p: &egui::Painter, hover: Pos2, fields: bool)             hover         }     };-    let Some(prompt) = app.session.current_prompt() else { return };+    let st = &app.session.settings;+    // Live values beside the cursor: pointer input unless DYNPIVIS 0 waits for typing, and+    // dimension input's distance and angle from the last point.+    let (pointer_values, dimension_values) = (st.dyn_pointer() && st.dynpivis != 0, st.dyn_dimension());+    let Some(prompt) = app.session.current_prompt() else {+        // DYNPIVIS 2: the cursor's coordinates even with no command running.+        if pointer_values+            && st.dynpivis == 2+            && app.cmd.buffer.is_empty()+            && let Some(c) = app.canvas.cursor+        {+            let galley = p.layout_no_wrap(+                format!("{}, {}", distance_text(&app.session, c.x), distance_text(&app.session, c.y)),+                crate::theme::small(),+                Tokens::get().text,+            );+            let r = tooltip_frame(p, at, galley.size() + vec2(12.0, 8.0));+            p.galley(r.min + vec2(6.0, 4.0), galley, Tokens::get().text);+        }+        return;+    };     // DYNPROMPT = 0: no prompt beside the cursor (the value boxes stay).-    let show_prompt = app.session.settings.dynprompt;+    let show_prompt = st.dynprompt;     let options = show_prompt && !prompt.keywords.is_empty();     if !fields         && let Some(f) = crate::cmdline::dyn_frame(app)+        && crate::cmdline::dyn_values_shown(app, &f)         && let Some(e) = crate::dyninput::parse(&app.cmd.buffer, &f)         && let Some(c) = app.canvas.cursor     {@@ -2371,14 +2392,14 @@ fn dynamic_input(app: &mut CadApp, p: &egui::Painter, hover: Pos2, fields: bool)         // The field shows the distance and what is typed.     } else if !app.cmd.buffer.is_empty() {         text = format!("{}: {}", crate::i18n::t(&prompt.message), app.cmd.buffer);-    } else if let (Some(base), Some(c)) = (prompt.base, app.canvas.cursor) {+    } else if let (Some(base), Some(c), true) = (prompt.base, app.canvas.cursor, pointer_values || dimension_values) {         text = format!(             "{}   {}  <  {}°",             crate::i18n::t(&prompt.message),             distance_text(&app.session, base.dist(c)),             (base.angle_to(c).to_degrees() * 10.0).round() / 10.0         );-    } else if let Some(c) = app.canvas.cursor {+    } else if let (Some(c), true) = (app.canvas.cursor, pointer_values) {         text = format!("{}   {}, {}", crate::i18n::t(&prompt.message), distance_text(&app.session, c.x), distance_text(&app.session, c.y));     }     let t = Tokens::get();diff --git a/crates/ui-egui/src/cmdline.rs b/crates/ui-egui/src/cmdline.rs--- a/crates/ui-egui/src/cmdline.rs+++ b/crates/ui-egui/src/cmdline.rs@@ -40,14 +40,27 @@ pub const MAX_LINES: usize = 12; const LINE_H: f32 = 16.0;  /// The Dynamic Input frame for the prompt on screen, or `None` when the value boxes are not live-/// (Dynamic Input off, no point prompt or a pending selection window).+/// (Dynamic Input off, no point prompt or a pending selection window). A first point takes pointer+/// input; next points take pointer input or dimension input, which is distance and angle from+/// the last point whatever DYNPIFORMAT and DYNPICOORDS say. pub fn dyn_frame(app: &CadApp) -> Option<crate::dyninput::Frame> {     let st = &app.session.settings;-    if !st.dynmode || app.session.pending_window.is_some() {+    if app.session.pending_window.is_some() {         return None;     }     let p = app.session.current_prompt().filter(cadcraft_engine::pointmod::accepts)?;-    Some(crate::dyninput::Frame { base: p.base, cartesian: st.dynpi_cartesian, absolute: st.dynpi_absolute })+    let pointer = st.dyn_pointer();+    if !pointer && !(p.base.is_some() && st.dyn_dimension()) {+        return None;+    }+    Some(crate::dyninput::Frame { base: p.base, cartesian: pointer && st.dynpi_cartesian, absolute: pointer && st.dynpi_absolute })+}++/// Whether the value boxes show before anything is typed: DYNPIVIS 0 keeps pointer input out of+/// sight until coordinates are typed (dimension input shows at once).+pub fn dyn_values_shown(app: &CadApp, f: &crate::dyninput::Frame) -> bool {+    let st = &app.session.settings;+    st.dynpivis != 0 || (f.base.is_some() && st.dyn_dimension()) || !app.cmd.buffer.trim().is_empty() }  /// Take Tab away from egui before it moves keyboard focus to the next widget (which would leave@@ -667,6 +680,42 @@ mod tests {         assert_eq!(app.ui.history_lines, 12);     } +    #[test]+    fn pointer_and_dimension_input_each_drive_the_value_boxes() {+        let mut app = CadApp::new(Session::new(), crate::Services::default());+        let set = |app: &mut CadApp, dynmode: i32| {+            app.session.settings.dynmode = dynmode > 0;+            app.session.settings.dyninput = dynmode.unsigned_abs() as u8;+        };+        app.session.settings.dynpi_cartesian = true;+        app.cmdline("line");+        // A first point takes pointer input only.+        set(&mut app, 2);+        assert!(dyn_frame(&app).is_none());+        set(&mut app, 1);+        assert!(dyn_frame(&app).is_some_and(|f| f.base.is_none()));+        app.cmdline("0,0");+        // Next points: dimension input alone is distance and angle from the last point.+        set(&mut app, 2);+        let f = dyn_frame(&app).expect("dimension input at a next point");+        assert!(f.base.is_some() && !f.cartesian && !f.absolute);+        set(&mut app, 3);+        assert!(dyn_frame(&app).is_some_and(|f| f.cartesian), "pointer input keeps DYNPIFORMAT");+        set(&mut app, -3);+        assert!(dyn_frame(&app).is_none(), "F12 off");+        // DYNPIVIS 0: pointer input waits for typing; dimension input shows at once.+        app.session.settings.dynpivis = 0;+        set(&mut app, 1);+        let f = dyn_frame(&app).expect("pointer input");+        assert!(!dyn_values_shown(&app, &f));+        app.cmd.buffer = "5".into();+        assert!(dyn_values_shown(&app, &f));+        app.cmd.buffer.clear();+        set(&mut app, 3);+        let f = dyn_frame(&app).expect("both inputs");+        assert!(dyn_values_shown(&app, &f));+    }+     fn frame(app: &mut CadApp, ctx: &egui::Context, events: Vec<Event>) {         let raw = egui::RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(1400.0, 900.0))), events, ..Default::default() };         let mut out = ctx.run_ui(raw, |ui| {diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs--- a/crates/ui-egui/src/context_menu.rs+++ b/crates/ui-egui/src/context_menu.rs@@ -267,7 +267,7 @@ pub fn snap_entries(app: &CadApp) -> Vec<Vec<Entry>> { /// Run an item of the object snap menu: type its modifier at the prompt, or open the settings. pub fn choose_snap(app: &mut CadApp, id: &str) {     if id == OSNAP_SETTINGS {-        activate(app, id);+        app.ui.dialog = Some("dsettings:osnap".into());     } else {         app.cmdline(id);     }diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs--- a/crates/ui-egui/src/dialogs.rs+++ b/crates/ui-egui/src/dialogs.rs@@ -19,7 +19,7 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {         "layers" => crate::layers::dialog(app, ctx, &mut open),         "qselect" => crate::quick::qselect_dialog(app, ctx, &mut open),         "parameters" => crate::parametric::parameters_dialog(app, ctx, &mut open),-        "dsettings" => dsettings(app, ctx, &mut open),+        o if o == "dsettings" || o.starts_with("dsettings:") => dsettings(app, ctx, o, &mut open),         "units" => units(app, ctx, &mut open),         "hatch" => crate::hatchdlg::dialog(app, ctx, &mut open),         "aliasedit" => crate::aliasdlg::dialog(app, ctx, &mut open),@@ -39,113 +39,335 @@ pub fn show(app: &mut CadApp, ctx: &egui::Context) {     } } -fn dsettings(app: &mut CadApp, ctx: &egui::Context, open: &mut bool) {+/// The Drafting Settings tabs: (id in the dialog name, label).+const DSETTINGS_TABS: [(&str, &str); 4] =+    [("snap", "Snap and Grid"), ("polar", "Polar Tracking"), ("osnap", "Object Snap"), ("dynamic", "Dynamic Input")];+const DSETTINGS_WIDTH: f32 = 460.0;+const DSETTINGS_MIN_TAB_HEIGHT: f32 = 300.0;+/// The ids of the windows the Dynamic Input tab's Settings... buttons open.+const POINTER_SETTINGS: &str = "dsettings_pointer";+const DIMENSION_SETTINGS: &str = "dsettings_dimension";++/// Drafting Settings, one tab at a time. The open tab is part of the dialog name+/// (`"dsettings:dynamic"`), as in Options, so a menu can open the tab it is about.+fn dsettings(app: &mut CadApp, ctx: &egui::Context, name: &str, open: &mut bool) {+    let tab = name.strip_prefix("dsettings:").and_then(|t| DSETTINGS_TABS.iter().find(|(id, _)| *id == t)).map_or("snap", |(id, _)| *id);     let settings = app.session.drafting_settings();     let (mut snapunit, mut gridunit) = (settings.snapunit, settings.gridunit);+    let mut next_tab = None;     egui::Window::new(crate::tl!("Drafting Settings")).id(egui::Id::new("Drafting Settings")).open(open).resizable(false).show(ctx, |ui| {+        ui.set_width(DSETTINGS_WIDTH);+        ui.horizontal_wrapped(|ui| {+            for (id, label) in DSETTINGS_TABS {+                if ui.selectable_label(tab == id, crate::i18n::t(label)).clicked() {+                    next_tab = Some(id);+                }+            }+        });+        ui.separator();+        let top = ui.cursor().top();         let s = &mut app.session.settings;-        ui.heading(crate::tl!("Snap and Grid"));-        ui.checkbox(&mut s.snapmode, crate::tl!("Snap On (F9)"));-        // SNAPSTYL: the isometric snap and grid (ISODRAFT).+        match tab {+            "polar" => polar_tab(ui, s),+            "osnap" => osnap_tab(ui, s),+            "dynamic" => dynamic_tab(ui, s),+            _ => snap_tab(ui, s, &mut snapunit, &mut gridunit),+        }+        // The same height on every tab, so switching tabs doesn't resize the dialog.+        ui.add_space((DSETTINGS_MIN_TAB_HEIGHT - (ui.cursor().top() - top)).max(0.0));+    });+    if let Some(t) = next_tab {+        app.ui.dialog = Some(format!("dsettings:{t}"));+    }+    if *open && tab == "dynamic" {+        pointer_settings(ctx, &mut app.session.settings);+        dimension_settings(ctx, &mut app.session.settings);+    } else {+        set_window_open(ctx, POINTER_SETTINGS, false);+        set_window_open(ctx, DIMENSION_SETTINGS, false);+    }+    // Drawing edits go through the same command as the CLI, including dirty state and undo.+    for (name, before, after) in [("SNAPUNIT", settings.snapunit, snapunit), ("GRIDUNIT", settings.gridunit, gridunit)] {+        if before != after {+            let _ = app.run("setvar", json!({ "name": name, "value": [after.x, after.y] }));+        }+    }+}++fn snap_tab(ui: &mut egui::Ui, s: &mut cadcraft_engine::Settings, snapunit: &mut cadcraft_geom::Vec2, gridunit: &mut cadcraft_geom::Vec2) {+    ui.checkbox(&mut s.snapmode, crate::tl!("Snap On (F9)"));+    // SNAPSTYL: the isometric snap and grid (ISODRAFT).+    ui.horizontal(|ui| {+        ui.radio_value(&mut s.isodraft, false, crate::tl!("Rectangular snap"));+        ui.radio_value(&mut s.isodraft, true, crate::tl!("Isometric snap"));+    });+    ui.horizontal(|ui| {+        ui.label(crate::tl!("Snap X spacing"));+        ui.add(egui::DragValue::new(&mut snapunit.x).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));+        ui.label(crate::tl!("Y"));+        ui.add(egui::DragValue::new(&mut snapunit.y).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));+    });+    // SNAPTYPE and POLARDIST.+    ui.horizontal(|ui| {+        ui.label(crate::tl!("Snap type"));+        ui.radio_value(&mut s.polarsnap, false, crate::tl!("Grid snap"));+        ui.radio_value(&mut s.polarsnap, true, crate::tl!("PolarSnap"));+    });+    ui.add_enabled_ui(s.polarsnap, |ui| {         ui.horizontal(|ui| {-            ui.radio_value(&mut s.isodraft, false, crate::tl!("Rectangular snap"));-            ui.radio_value(&mut s.isodraft, true, crate::tl!("Isometric snap"));+            ui.label(crate::tl!("Polar distance"));+            ui.add(egui::DragValue::new(&mut s.polardist).speed(0.05).range(0.0..=1e6));         });-        ui.horizontal(|ui| {-            ui.label(crate::tl!("Snap X spacing"));-            ui.add(egui::DragValue::new(&mut snapunit.x).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));-            ui.label(crate::tl!("Y"));-            ui.add(egui::DragValue::new(&mut snapunit.y).speed(0.05).range(0.0001..=1e6).clamp_existing_to_range(false));+    });+    ui.checkbox(&mut s.gridmode, crate::tl!("Grid On (F7)"));+    ui.horizontal(|ui| {+        ui.label(crate::tl!("Grid spacing"));+        ui.add(egui::DragValue::new(&mut gridunit.x).speed(0.05).range(0.0..=1e6).clamp_existing_to_range(false));+        ui.label(crate::tl!("Major line every"));+        ui.add(egui::DragValue::new(&mut s.gridmajor).range(1..=100));+    });+}++fn polar_tab(ui: &mut egui::Ui, s: &mut cadcraft_engine::Settings) {+    ui.checkbox(&mut s.polarmode, crate::tl!("Polar Tracking On (F10)"));+    // POLARANG is held in radians and 15 or 30 come back as 14.999999999999998: round like+    // the additional angles, and write back only a newly picked angle.+    let current = (s.polarang.to_degrees() * 1e6).round() / 1e6;+    let mut deg = current;+    ui.horizontal(|ui| {+        ui.label(crate::tl!("Increment angle"));+        egui::ComboBox::from_id_salt("polarang").selected_text(format!("{deg}")).show_ui(ui, |ui| {+            for a in [90.0, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0] {+                ui.selectable_value(&mut deg, a, format!("{a}"));+            }         });-        // SNAPTYPE and POLARDIST.+    });+    if deg != current {+        s.polarang = deg.to_radians();+    }+    tracking_options(ui, s);+    ui.separator();+    ui.checkbox(&mut s.orthomode, crate::tl!("Ortho (F8)"));+}++fn osnap_tab(ui: &mut egui::Ui, s: &mut cadcraft_engine::Settings) {+    let mut on = s.osmode & cadcraft_engine::snap::mode::OFF == 0;+    if ui.checkbox(&mut on, crate::tl!("Object Snap On (F3)")).changed() {+        if on {+            s.osmode &= !cadcraft_engine::snap::mode::OFF;+        } else {+            s.osmode |= cadcraft_engine::snap::mode::OFF;+        }+    }+    egui::Grid::new("osnap_grid").num_columns(2).show(ui, |ui| {+        for (i, (bit, name)) in cadcraft_engine::snap::mode::ALL.iter().enumerate() {+            let mut v = s.osmode & bit != 0;+            if ui.checkbox(&mut v, crate::i18n::t(name)).changed() {+                if v {+                    s.osmode |= bit;+                } else {+                    s.osmode &= !bit;+                }+            }+            if i % 2 == 1 {+                ui.end_row();+            }+        }+    });+    ui.separator();+    ui.checkbox(&mut s.otrack, crate::tl!("Object Snap Tracking On (F11)"));+}++/// The Dynamic Input tab: pointer input and dimension input (DYNMODE's bits, each with its+/// settings window) and dynamic prompts (DYNPROMPT).+fn dynamic_tab(ui: &mut egui::Ui, s: &mut cadcraft_engine::Settings) {+    // While F12 has turned Dynamic Input off (DYNMODE negative) both show unticked; ticking one+    // turns it back on with that input.+    let shown = if s.dynmode { s.dyninput } else { 0 };+    let mut bits = shown;+    ui.columns(2, |cols| {+        if let [left, right] = cols {+            input_group(left, &mut bits, 1, crate::tl!("Enable Pointer Input"), crate::tl!("Pointer Input"), Preview::Pointer, POINTER_SETTINGS);+            input_group(+                right,+                &mut bits,+                2,+                crate::tl!("Enable Dimension Input where possible"),+                crate::tl!("Dimension Input"),+                Preview::Dimension,+                DIMENSION_SETTINGS,+            );+        }+    });+    if bits != shown {+        s.dynmode = bits != 0;+        s.dyninput = bits;+    }+    ui.add_space(6.0);+    ui.group(|ui| {+        ui.set_width(ui.available_width());+        ui.label(crate::tl!("Dynamic Prompts"));         ui.horizontal(|ui| {-            ui.label(crate::tl!("Snap type"));-            ui.radio_value(&mut s.polarsnap, false, crate::tl!("Grid snap"));-            ui.radio_value(&mut s.polarsnap, true, crate::tl!("PolarSnap"));+            preview(ui, Preview::Prompt);+            // DYNPROMPT: the prompt and its options (Down Arrow) beside the cursor.+            ui.checkbox(&mut s.dynprompt, crate::tl!("Show command prompting near the crosshairs"));         });-        ui.add_enabled_ui(s.polarsnap, |ui| {+    });+}++/// One of the Dynamic Input tab's inputs: its Enable box (DYNMODE `bit`), and a group with a+/// picture of it and the Settings... button that opens its window.+fn input_group(ui: &mut egui::Ui, bits: &mut u8, bit: u8, enable: &str, title: &str, kind: Preview, window: &str) {+    let mut on = *bits & bit != 0;+    if ui.checkbox(&mut on, enable).changed() {+        *bits ^= bit;+    }+    ui.group(|ui| {+        ui.set_width(ui.available_width());+        ui.label(title);+        preview(ui, kind);+        ui.vertical_centered(|ui| {+            if ui.button(crate::tl!("Settings...")).clicked() {+                set_window_open(ui.ctx(), window, true);+            }+        });+    });+}++fn window_open(ctx: &egui::Context, window: &str) -> bool {+    ctx.data(|d| d.get_temp::<bool>(egui::Id::new(window))).unwrap_or(false)+}++fn set_window_open(ctx: &egui::Context, window: &str, open: bool) {+    ctx.data_mut(|d| d.insert_temp(egui::Id::new(window), open));+}++/// What a Dynamic Input picture shows.+#[derive(Clone, Copy)]+enum Preview {+    /// The crosshair with its X and Y boxes.+    Pointer,+    /// A rubber band from the last point with its length field.+    Dimension,+    /// The crosshair with a prompt beside it.+    Prompt,+}++/// A small picture of what an input shows beside the cursor, drawn like the drawing area.+fn preview(ui: &mut egui::Ui, kind: Preview) {+    use egui::{Pos2, Rect, Stroke};+    let t = Tokens::get();+    let (r, _) = ui.allocate_exact_size(vec2(ui.available_width().min(200.0), 72.0), egui::Sense::hover());+    let p = ui.painter_at(r);+    p.rect_filled(r, 3.0, t.canvas);+    let ink = Stroke::new(1.0, t.crosshair);+    let cross = |c: Pos2| {+        p.line_segment([c - vec2(16.0, 0.0), c + vec2(16.0, 0.0)], ink);+        p.line_segment([c - vec2(0.0, 16.0), c + vec2(0.0, 16.0)], ink);+    };+    let tip = |at: Pos2, text: String| {+        let g = p.layout_no_wrap(text, crate::theme::small(), t.text);+        let b = Rect::from_min_size(at, g.size() + vec2(8.0, 4.0));+        p.rect_filled(b, 2.0, t.cmd_bg);+        p.rect_stroke(b, 2.0, Stroke::new(1.0, t.cmd_border), egui::StrokeKind::Inside);+        p.galley(b.min + vec2(4.0, 2.0), g, t.text);+        b+    };+    match kind {+        Preview::Pointer => {+            let c = r.left_center() + vec2(36.0, -10.0);+            cross(c);+            let b = tip(c + vec2(14.0, 8.0), "12.5000".into());+            tip(egui::pos2(b.right() + 4.0, b.top()), "8.2500".into());+        }+        Preview::Dimension => {+            let a = r.left_bottom() + vec2(16.0, -14.0);+            let c = r.right_top() + vec2(-40.0, 24.0);+            p.line_segment([a, c], Stroke::new(1.0, t.accent));+            cross(c);+            // The dimension line beside the rubber band, its length field halfway along.+            let n = (c - a).normalized().rot90() * 12.0;+            let dim = Stroke::new(1.0, t.text_dim);+            p.line_segment([a, a + n * 1.3], dim);+            p.line_segment([c, c + n * 1.3], dim);+            p.line_segment([a + n, c + n], dim);+            let size = p.layout_no_wrap("4.2500".into(), crate::theme::small(), t.text).size() + vec2(8.0, 4.0);+            tip(a + (c - a) * 0.5 + n - size * 0.5, "4.2500".into());+        }+        Preview::Prompt => {+            let c = r.left_center() + vec2(30.0, -8.0);+            cross(c);+            tip(c + vec2(14.0, 8.0), format!("{}:", crate::i18n::t("Specify first point")));+        }+    }+}++/// Pointer Input settings: the format of second and next points (DYNPIFORMAT, DYNPICOORDS) and+/// when the coordinates show (DYNPIVIS).+fn pointer_settings(ctx: &egui::Context, s: &mut cadcraft_engine::Settings) {+    let mut open = window_open(ctx, POINTER_SETTINGS);+    if !open {+        return;+    }+    egui::Window::new(crate::tl!("Pointer Input Settings"))+        .id(egui::Id::new(POINTER_SETTINGS))+        .open(&mut open)+        .resizable(false)+        .collapsible(false)+        .show(ctx, |ui| {+            ui.label(crate::tl!("Second and next points:"));             ui.horizontal(|ui| {-                ui.label(crate::tl!("Polar distance"));-                ui.add(egui::DragValue::new(&mut s.polardist).speed(0.05).range(0.0..=1e6));+                ui.radio_value(&mut s.dynpi_cartesian, false, crate::tl!("Polar (distance < angle)"));+                ui.radio_value(&mut s.dynpi_cartesian, true, crate::tl!("Cartesian (x, y)"));             });-        });-        ui.checkbox(&mut s.gridmode, crate::tl!("Grid On (F7)"));-        ui.horizontal(|ui| {-            ui.label(crate::tl!("Grid spacing"));-            ui.add(egui::DragValue::new(&mut gridunit.x).speed(0.05).range(0.0..=1e6).clamp_existing_to_range(false));-            ui.label(crate::tl!("Major line every"));-            ui.add(egui::DragValue::new(&mut s.gridmajor).range(1..=100));-        });-        ui.separator();-        ui.heading(crate::tl!("Polar Tracking"));-        ui.checkbox(&mut s.polarmode, crate::tl!("Polar Tracking On (F10)"));-        // POLARANG is held in radians and 15 or 30 come back as 14.999999999999998: round like-        // the additional angles, and write back only a newly picked angle.-        let current = (s.polarang.to_degrees() * 1e6).round() / 1e6;-        let mut deg = current;-        ui.horizontal(|ui| {-            ui.label(crate::tl!("Increment angle"));-            egui::ComboBox::from_id_salt("polarang").selected_text(format!("{deg}")).show_ui(ui, |ui| {-                for a in [90.0, 45.0, 30.0, 22.5, 18.0, 15.0, 10.0, 5.0] {-                    ui.selectable_value(&mut deg, a, format!("{a}"));-                }+            ui.horizontal(|ui| {+                ui.radio_value(&mut s.dynpi_absolute, false, crate::tl!("Relative"));+                ui.radio_value(&mut s.dynpi_absolute, true, crate::tl!("Absolute"));             });+            ui.separator();+            ui.label(crate::tl!("Show coordinate tooltips:"));+            ui.radio_value(&mut s.dynpivis, 0, crate::tl!("Once coordinates are typed"));+            ui.radio_value(&mut s.dynpivis, 1, crate::tl!("When a command asks for a point"));+            ui.radio_value(&mut s.dynpivis, 2, crate::tl!("Always, even with no command running"));         });-        if deg != current {-            s.polarang = deg.to_radians();-        }-        tracking_options(ui, s);-        ui.separator();-        ui.heading(crate::tl!("Object Snap"));-        let mut on = s.osmode & cadcraft_engine::snap::mode::OFF == 0;-        if ui.checkbox(&mut on, crate::tl!("Object Snap On (F3)")).changed() {-            if on {-                s.osmode &= !cadcraft_engine::snap::mode::OFF;-            } else {-                s.osmode |= cadcraft_engine::snap::mode::OFF;-            }-        }-        egui::Grid::new("osnap_grid").num_columns(2).show(ui, |ui| {-            for (i, (bit, name)) in cadcraft_engine::snap::mode::ALL.iter().enumerate() {-                let mut v = s.osmode & bit != 0;-                if ui.checkbox(&mut v, crate::i18n::t(name)).changed() {-                    if v {-                        s.osmode |= bit;-                    } else {-                        s.osmode &= !bit;+    set_window_open(ctx, POINTER_SETTINGS, open);+}++/// Dimension Input settings: which dimensions show while a grip is stretched (DYNDIVIS, and+/// DYNDIGRIP's bits when it is 2).+fn dimension_settings(ctx: &egui::Context, s: &mut cadcraft_engine::Settings) {+    let mut open = window_open(ctx, DIMENSION_SETTINGS);+    if !open {+        return;+    }+    egui::Window::new(crate::tl!("Dimension Input Settings"))+        .id(egui::Id::new(DIMENSION_SETTINGS))+        .open(&mut open)+        .resizable(false)+        .collapsible(false)+        .show(ctx, |ui| {+            ui.label(crate::tl!("While a grip is stretched, show:"));+            ui.radio_value(&mut s.dyndivis, 0, crate::tl!("One dimension input field at a time"));+            ui.radio_value(&mut s.dyndivis, 1, crate::tl!("Two dimension input fields at a time"));+            ui.radio_value(&mut s.dyndivis, 2, crate::tl!("These dimension input fields together:"));+            ui.add_enabled_ui(s.dyndivis == 2, |ui| {+                ui.indent("dyndigrip", |ui| {+                    for (bit, label) in [+                        (1, crate::tl!("Resulting Dimension")),+                        (2, crate::tl!("Length Change")),+                        (4, crate::tl!("Absolute Angle")),+                        (8, crate::tl!("Angle Change")),+                        (16, crate::tl!("Arc Radius")),+                    ] {+                        let mut on = s.dyndigrip & bit != 0;+                        if ui.checkbox(&mut on, label).changed() {+                            s.dyndigrip ^= bit;+                        }                     }-                }-                if i % 2 == 1 {-                    ui.end_row();-                }-            }-        });-        ui.separator();-        ui.checkbox(&mut s.dynmode, crate::tl!("Enable Dynamic Input (F12)"));-        // Pointer input format for second and next points (DYNPIFORMAT, DYNPICOORDS).-        ui.indent("dynpi", |ui| {-            ui.add_enabled_ui(s.dynmode, |ui| {-                ui.label(crate::tl!("Second and next points:"));-                ui.horizontal(|ui| {-                    ui.radio_value(&mut s.dynpi_cartesian, false, crate::tl!("Polar (distance < angle)"));-                    ui.radio_value(&mut s.dynpi_cartesian, true, crate::tl!("Cartesian (x, y)"));-                });-                ui.horizontal(|ui| {-                    ui.radio_value(&mut s.dynpi_absolute, false, crate::tl!("Relative"));-                    ui.radio_value(&mut s.dynpi_absolute, true, crate::tl!("Absolute"));                 });-                // DYNPROMPT: the prompt and its options (Down Arrow) beside the cursor.-                ui.checkbox(&mut s.dynprompt, crate::tl!("Show command prompting near the crosshairs"));             });         });-        ui.checkbox(&mut s.orthomode, crate::tl!("Ortho (F8)"));-    });-    // Drawing edits go through the same command as the CLI, including dirty state and undo.-    for (name, before, after) in [("SNAPUNIT", settings.snapunit, snapunit), ("GRIDUNIT", settings.gridunit, gridunit)] {-        if before != after {-            let _ = app.run("setvar", json!({ "name": name, "value": [after.x, after.y] }));-        }-    }+    set_window_open(ctx, DIMENSION_SETTINGS, open); }  /// The polar and object snap tracking options of Drafting Settings: additional anglesdiff --git a/crates/ui-egui/src/grip_dyn.rs b/crates/ui-egui/src/grip_dyn.rs--- a/crates/ui-egui/src/grip_dyn.rs+++ b/crates/ui-egui/src/grip_dyn.rs@@ -33,8 +33,11 @@ pub struct GripDyn { }  /// The base of the prompt on show when it asks for a distance from it (a circle's radius, a-/// polyline arc's): its one field is that distance.+/// polyline arc's): its one field is that distance (dimension input). fn distance_base(app: &CadApp) -> Option<Vec2> {+    if !app.session.settings.dyn_dimension() {+        return None;+    }     crate::cmdline::dyn_frame(app).and_then(|_| app.session.current_prompt()).filter(|p| p.distance).and_then(|p| p.base) } diff --git a/crates/ui-egui/tests/it/drafting_dynamic_input.rs b/crates/ui-egui/tests/it/drafting_dynamic_input.rsnew file mode 100644--- /dev/null+++ b/crates/ui-egui/tests/it/drafting_dynamic_input.rs@@ -0,0 +1,80 @@+//! Drafting Settings opens on its tabs, and the Dynamic Input tab turns pointer input and+//! dimension input on and off apart (DYNMODE's bits) and opens their settings windows.++use cadcraft_engine::Session;+use cadcraft_ui_egui::{CadApp, Services, dialogs, theme};+use egui_kittest::Harness;+use egui_kittest::kittest::Queryable;+use serde_json::json;++fn harness(dialog: &str) -> Harness<'static, CadApp> {+    let mut app = CadApp::new(Session::new(), Services::default());+    app.ui.dialog = Some(dialog.into());+    let mut h = Harness::builder().with_size(egui::vec2(900.0, 700.0)).build_ui_state(+        |ui, app: &mut CadApp| {+            theme::apply(ui.ctx(), egui::Theme::Dark, false);+            dialogs::show(app, ui.ctx());+        },+        app,+    );+    h.run_steps(5);+    h+}++fn dynmode(h: &mut Harness<'_, CadApp>) -> serde_json::Value {+    h.state_mut().run("getvar", json!({"name": "DYNMODE"})).unwrap()["value"].clone()+}++#[test]+fn the_tabs_switch_and_keep_the_dialog_open() {+    let mut h = harness("dsettings");+    assert!(h.query_by_label("Snap On (F9)").is_some(), "Snap and Grid is the first tab");+    h.get_by_label("Object Snap").click();+    h.run_steps(3);+    assert_eq!(h.state().ui.dialog.as_deref(), Some("dsettings:osnap"));+    assert!(h.query_by_label("Object Snap On (F3)").is_some());+    assert!(h.query_by_label("Snap On (F9)").is_none());+    h.get_by_label("Dynamic Input").click();+    h.run_steps(3);+    assert!(h.query_by_label("Enable Pointer Input").is_some());+}++#[test]+fn pointer_and_dimension_input_tick_apart() {+    let mut h = harness("dsettings:dynamic");+    assert_eq!(dynmode(&mut h), json!(3));+    h.get_by_label("Enable Pointer Input").click();+    h.run_steps(3);+    assert_eq!(dynmode(&mut h), json!(2), "dimension input stays on");+    h.get_by_label("Enable Dimension Input where possible").click();+    h.run_steps(3);+    assert_eq!(dynmode(&mut h), json!(0));+    h.get_by_label("Enable Pointer Input").click();+    h.run_steps(3);+    assert_eq!(dynmode(&mut h), json!(1));+    // F12 off: both show unticked; ticking one turns Dynamic Input back on with it.+    h.state_mut().run("dynmode", json!({"on": false})).unwrap();+    h.run_steps(3);+    h.get_by_label("Enable Dimension Input where possible").click();+    h.run_steps(3);+    assert_eq!(dynmode(&mut h), json!(2));+}++#[test]+fn settings_buttons_open_the_pointer_and_dimension_windows() {+    let mut h = harness("dsettings:dynamic");+    h.get_all_by_label("Settings...").next().expect("Pointer Input's Settings...").click();+    h.run_steps(3);+    h.get_by_label("Always, even with no command running").click();+    h.run_steps(3);+    assert_eq!(h.state_mut().run("getvar", json!({"name": "DYNPIVIS"})).unwrap()["value"], json!(2));+    h.get_all_by_label("Settings...").nth(1).expect("Dimension Input's Settings...").click();+    h.run_steps(3);+    h.get_by_label("These dimension input fields together:").click();+    h.run_steps(3);+    h.get_by_label("Arc Radius").click();+    h.run_steps(3);+    let app = h.state_mut();+    assert_eq!(app.run("getvar", json!({"name": "DYNDIVIS"})).unwrap()["value"], json!(2));+    assert_eq!(app.run("getvar", json!({"name": "DYNDIGRIP"})).unwrap()["value"], json!(15));+}diff --git a/crates/ui-egui/tests/it/drafting_polar_increment.rs b/crates/ui-egui/tests/it/drafting_polar_increment.rs--- a/crates/ui-egui/tests/it/drafting_polar_increment.rs+++ b/crates/ui-egui/tests/it/drafting_polar_increment.rs@@ -11,7 +11,7 @@ fn shown_increment(degrees: f64) -> (String, f64, f64) {     let mut app = CadApp::new(Session::new(), Services::default());     app.run("setvar", json!({"name": "POLARANG", "value": degrees})).unwrap();     let stored = app.session.settings.polarang;-    app.ui.dialog = Some("dsettings".into());+    app.ui.dialog = Some("dsettings:polar".into());     let mut h = Harness::builder().with_size(egui::vec2(800.0, 600.0)).build_ui_state(         |ui, app: &mut CadApp| {             theme::apply(ui.ctx(), egui::Theme::Dark, false);diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs--- a/crates/ui-egui/tests/it/main.rs+++ b/crates/ui-egui/tests/it/main.rs@@ -7,6 +7,7 @@ mod control_cmdline_key; mod control_methods; mod dialog_dispatch; mod docking_workflows;+mod drafting_dynamic_input; mod drafting_polar_increment; mod drafting_spacing; mod file_tab_titles;diff --git a/docs/ui-parity.md b/docs/ui-parity.md--- a/docs/ui-parity.md+++ b/docs/ui-parity.md@@ -1,6 +1,6 @@ # UI parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-22 · **Change:** minor (distinct polyline midpoint grips) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (Drafting Settings tabs; pointer and dimension input apart) · **Target:** Autodesk AutoCAD 2027 for Mac (26.0)  How CADCraft's interaction compares with AutoCAD's: point entry, object snaps, tracking, dynamic input, grips, selection, the command line, chrome and navigation. A drafter's speed in AutoCAD@@ -91,7 +91,7 @@ ignored; there are no context menus. Remaining: **≈ 110–170 Opus 5.5 hours** | Grid (rectangular, major lines, adaptive, limits) | yes | rectangular and isometric (30°, 90° and 150° lines), major lines, adaptive display | | Snap (rectangular, isometric, polar snap) | yes | rectangular and isometric (SNAPSTYL 1: the snap Y spacing along the isometric axes), from SNAPBASE turned by SNAPANG | | Isometric drafting (ISODRAFT, isoplanes F5) | yes | yes (#556): ISODRAFT and the status-bar toggle set SNAPSTYL and the plane; ISOPLANE / F5 / Ctrl+E cycle Left, Top, Right (SNAPISOPAIR); ortho, direct distance entry and the crosshair follow the plane's axes; ELLIPSE Isocircle (radius or Diameter, also for elliptical arcs); Drafting Settings snap style radio. Engine: `snap/iso.rs`, `cmd/settings/iso.rs` |-| Drafting Settings dialog | yes | yes (`dialogs.rs:36-80`) |+| Drafting Settings dialog | yes | yes (`dialogs.rs`): Snap and Grid, Polar Tracking, Object Snap and Dynamic Input tabs (`dsettings:<tab>` opens one; Osnap Settings opens Object Snap). Dynamic Input: Enable Pointer Input and Enable Dimension Input (DYNMODE 1/2/3, negative while F12 has it off), each with a picture and a Settings window (pointer: DYNPIFORMAT, DYNPICOORDS, DYNPIVIS; dimension: DYNDIVIS, DYNDIGRIP), and Dynamic Prompts (DYNPROMPT); no additional tips (DYNINFOTIPS) or Drafting Tooltip Appearance | | Hatch and Gradient / Hatch Edit dialogs | yes | yes (`hatchdlg.rs`): pattern swatches, gradient colours and shapes, origin, pick points or objects, associative, separate, island detection; Hatch Edit adds boundaries from points or closed objects and removes selected loops; opened by HATCH, GRADIENT (HPDLGMODE), HATCHEDIT, right-click or double-clicking a hatch; no Preview or Inherit Properties |  ## Dynamic input (≈ 45%)@@ -203,6 +203,7 @@ down-arrow options menu (DYNPROMPT), and DYNDIM.  | Date | Change | Summary | |---|---|---|+| 2026-10-25 | minor | Drafting Settings in tabs, with a Dynamic Input tab: pointer input and dimension input turn on apart (DYNMODE bits), DYNPIVIS, and Settings windows for both | | 2026-10-22 | minor | Polyline segment midpoint grips use rectangles, distinct from square vertex grips, with matching hover and pick bounds (#1950) | | 2026-10-13 | minor | Filled-region highlight boundaries are traversed in deterministic coordinate order, fixing dash-phase jumps on CPU hover/selection redraws and GPU highlight rebuilds; CPU dash-position, pixel and GPU mesh regressions | | 2026-10-14 | minor | Selection cycling (#1143): the list and badge of SELECTIONCYCLING, Shift+Space through overlapping objects (#1145) |