From 8d71f7de4a24d920e088edf0fcd1796e7b65bc12 Mon Sep 17 00:00:00 2001 From: Valentin Klein Date: Sat, 10 Oct 2026 18:15:34 +0200 Subject: [PATCH] Properties palette: every control acts or is visibly unavailable (#182) * Properties palette: every control acts or is visibly unavailable (#6) Several Properties palette controls looked clickable but did nothing: most current-property rows with nothing selected (linetype, lineweight, thickness, text/dimension/multileader/table style), the selection-type drop-down, a circle's circumference and area, a line's length and angle, a dimension's style, and the All/My switch. - Nothing selected: Linetype runs `linetype {current}`, Lineweight `lweight`, Thickness `setvar THICKNESS`, the style rows `style.current`, `dimstyle.current`, `mleaderstyle` and `tablestyle`. Transparency, Annotation scale and Plot style(s) have no command yet: greyed with a "Not available yet" tooltip. - Header: the type drop-down narrows the selection to one object type (`select {handles}`), plus Toggle PICKADD (`setvar PICKADD`), Select Objects (`select`) and Quick Select buttons, with icons drawn in code. - Geometry: circle circumference/area, line length/angle and dimension style are editable through `properties.set` (new keys `circumference`, `area`, `dimStyle`); computed values (line deltas, polyline length/area, block name) are greyed read-only. - PICKADD now changes clicking with no command running: off, each pick replaces the selection and Shift adds. - Palette text fields keep typed text while the window itself is unfocused (they read focus from egui memory, not `Response::has_focus`). Co-Authored-By: Claude Opus 5.5 * Move palette tests away from other PRs' hunks to avoid merge conflicts Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/props.rs | 13 ++ crates/engine/src/lib.rs | 18 +- crates/engine/src/tests.rs | 91 +++++++++ crates/ui-egui/src/icons.rs | 19 ++ crates/ui-egui/src/palettes.rs | 331 +++++++++++++++++++++------------ 5 files changed, 350 insertions(+), 122 deletions(-) diff --git a/crates/engine/src/cmd/props.rs b/crates/engine/src/cmd/props.rs index 16600c97a..6b446194e 100644 --- a/crates/engine/src/cmd/props.rs +++ b/crates/engine/src/cmd/props.rs @@ -172,6 +172,9 @@ fn run_set(s: &mut Session, p: &Value) -> Result { if let Some(l) = str_param(p, "layer") { d.ensure_layer(l); } + let dim_style = str_param(p, "dimStyle") + .map(|n| d.dim_style(n).map(|st| st.name.clone()).ok_or_else(|| bad("properties.set", format!("dimension style `{n}` not found")))) + .transpose()?; for h in &hs { d.modify_entity(*h, |e| { if let Some(l) = str_param(p, "layer") { @@ -226,6 +229,12 @@ fn run_set(s: &mut Session, p: &Value) -> Result { if let Some(dm) = num("diameter").filter(|r| *r > 0.0) { c.radius = dm / 2.0; } + if let Some(cf) = num("circumference").filter(|x| *x > 0.0) { + c.radius = cf / cadcraft_geom::TAU; + } + if let Some(a) = num("area").filter(|x| *x > 0.0) { + c.radius = (a / cadcraft_geom::PI).sqrt(); + } } EntityKind::Arc(a) => { if let Some(v) = p.get("center") { @@ -293,6 +302,10 @@ fn run_set(s: &mut Session, p: &Value) -> Result { dm.text = v.to_string(); dm.block = None; } + if let Some(st) = &dim_style { + dm.style.clone_from(st); + dm.block = None; + } } EntityKind::Hatch(h) => { if let Some(v) = str_param(p, "pattern") { diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 96691baf5..5c1c3119f 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -855,15 +855,18 @@ impl Session { // ---------------- picking without a command ---------------- /// A click with no command running: pick/toggle objects or start a selection window. + /// + /// PICKADD on (the default): picks add to the selection and Shift removes. PICKADD off: each pick + /// replaces the selection and Shift adds (Shift+pick on a selected object removes it). pub fn idle_click(&mut self, p: Vec2, shift: bool) -> Result<()> { let space = self.space(); + let pickadd = self.settings.pickadd; if let Some(pw) = self.pending_window.take() { let crossing = p.x < pw.corner.x; let hs = select::select_window(self.doc()?, &space, Bounds2::new(pw.corner, p), crossing); - let mut sel = if shift { Vec::new() } else { self.selection() }; - if shift { - let cur = self.selection(); - sel = cur.into_iter().filter(|h| !hs.contains(h)).collect(); + let mut sel = if pickadd || shift { self.selection() } else { Vec::new() }; + if shift && pickadd { + sel.retain(|h| !hs.contains(h)); } else { sel.extend(hs); } @@ -873,10 +876,11 @@ impl Session { let ap = self.pixel_size() * self.settings.pickbox.max(1.0) * 1.5; match select::pick(self.doc()?, &space, p, ap) { Some(h) => { - let mut sel = self.selection(); - if shift { + let mut sel = if pickadd || shift { self.selection() } else { Vec::new() }; + let had = sel.contains(&h); + if shift && (pickadd || had) { sel.retain(|x| *x != h); - } else if !sel.contains(&h) { + } else if !had { sel.push(h); } self.set_selection(sel); diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index c0bf38837..650f035a6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -183,6 +183,97 @@ fn extend_line_to_boundary() { assert!(g[2].near(Vec2::new(8.0, 0.0), 1e-9)); } +#[test] +fn properties_set_circle_size_and_dim_style() { + // Issue #6: the Properties palette edits a circle's circumference/area and a dimension's style. + let mut s = Session::new(); + s.execute("circle", &json!({"center": [0, 0], "radius": 1})).unwrap(); + let c = s.doc().unwrap().model.last().unwrap().handle; + let radius = |s: &Session| match &s.doc().unwrap().entity(c).unwrap().kind { + EntityKind::Circle(c) => c.radius, + _ => f64::NAN, + }; + s.execute("properties.set", &json!({"handles": [c.hex()], "circumference": 4.0 * std::f64::consts::PI})).unwrap(); + assert!((radius(&s) - 2.0).abs() < 1e-12); + s.execute("properties.set", &json!({"handles": [c.hex()], "area": 9.0 * std::f64::consts::PI})).unwrap(); + assert!((radius(&s) - 3.0).abs() < 1e-12); + // Zero, negative or non-numeric sizes are ignored. + for v in [json!(0), json!(-5), json!("NaN"), json!(null)] { + s.execute("properties.set", &json!({"handles": [c.hex()], "circumference": v, "area": v})).unwrap(); + assert!((radius(&s) - 3.0).abs() < 1e-12, "{v}"); + } + s.execute("properties.set", &json!({"handles": [c.hex()], "area": 1e308})).unwrap(); + assert!(radius(&s).is_finite()); + + s.execute("dimstyle", &json!({"name": "Big", "current": false})).unwrap(); + s.execute("dimlinear", &json!({"p1": [0, 0], "p2": [3, 8], "at": [6, 4]})).unwrap(); + let dm = s.doc().unwrap().model.last().unwrap().handle; + let style = |s: &Session| match &s.doc().unwrap().entity(dm).unwrap().kind { + EntityKind::Dimension(d) => d.style.clone(), + _ => String::new(), + }; + s.execute("properties.set", &json!({"handles": [dm.hex()], "dimStyle": "big"})).unwrap(); + assert_eq!(style(&s), "Big"); + assert!(s.execute("properties.set", &json!({"handles": [dm.hex()], "dimStyle": "Nope"})).is_err()); + assert_eq!(style(&s), "Big"); +} + +#[test] +fn properties_palette_current_property_commands() { + // Issue #6: the commands the Properties palette runs with nothing selected. + let mut s = Session::new(); + s.execute("linetype", &json!({"current": "ByBlock"})).unwrap(); + s.execute("lweight", &json!({"lineweight": 0.5})).unwrap(); + s.execute("setvar", &json!({"name": "THICKNESS", "value": 2.5})).unwrap(); + s.execute("dimstyle", &json!({"name": "Big", "current": false})).unwrap(); + s.execute("dimstyle.current", &json!({"name": "Big"})).unwrap(); + s.execute("mleaderstyle", &json!({"name": "Callout", "current": false})).unwrap(); + s.execute("mleaderstyle", &json!({"name": "Callout", "current": true})).unwrap(); + s.execute("tablestyle", &json!({"name": "Schedule", "current": false})).unwrap(); + s.execute("tablestyle", &json!({"name": "Schedule", "current": true})).unwrap(); + s.execute("style", &json!({"name": "Notes", "current": false})).unwrap(); + s.execute("style.current", &json!({"name": "Notes"})).unwrap(); + let h = &s.doc().unwrap().header; + assert_eq!(h.str("CELTYPE", ""), "ByBlock"); + assert_eq!(h.i64("CELWEIGHT", 0), 50); + assert_eq!(h.f64("THICKNESS", 0.0), 2.5); + assert_eq!(h.str("DIMSTYLE", ""), "Big"); + assert_eq!(h.str("CMLEADERSTYLE", ""), "Callout"); + assert_eq!(h.str("CTABLESTYLE", ""), "Schedule"); + assert_eq!(h.str("TEXTSTYLE", ""), "Notes"); +} + +#[test] +fn pickadd_controls_idle_picks() { + // Issue #6: the palette's PICKADD toggle must change how clicks select. + let mut s = Session::new(); + s.execute("line", &json!({"points": [[0, 0], [10, 0]]})).unwrap(); + let a = s.doc().unwrap().model.last().unwrap().handle; + s.execute("line", &json!({"points": [[0, 5], [10, 5]]})).unwrap(); + let b = s.doc().unwrap().model.last().unwrap().handle; + let (pa, pb) = (Vec2::new(5.0, 0.0), Vec2::new(5.0, 5.0)); + // PICKADD on: picks add, Shift removes. + s.idle_click(pa, false).unwrap(); + s.idle_click(pb, false).unwrap(); + assert_eq!(s.selection(), vec![a, b]); + s.idle_click(pa, true).unwrap(); + assert_eq!(s.selection(), vec![b]); + // PICKADD off: each pick replaces, Shift adds, Shift on a selected object removes it. + s.execute("setvar", &json!({"name": "PICKADD", "value": 0})).unwrap(); + s.idle_click(pa, false).unwrap(); + assert_eq!(s.selection(), vec![a]); + s.idle_click(pb, true).unwrap(); + assert_eq!(s.selection(), vec![a, b]); + s.idle_click(pa, true).unwrap(); + assert_eq!(s.selection(), vec![b]); + s.idle_click(pa, false).unwrap(); + assert_eq!(s.selection(), vec![a]); + // A window replaces the selection too. + s.idle_click(Vec2::new(-1.0, 4.0), false).unwrap(); + s.idle_click(Vec2::new(11.0, 6.0), false).unwrap(); + assert_eq!(s.selection(), vec![b]); +} + #[test] fn fillet_two_lines() { let mut s = Session::new(); diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index 595896461..4e043159f 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -139,6 +139,9 @@ pub enum Icon { Distance, Area, List, + PickAdd, + SelectObjects, + QuickSelect, } struct Pen<'a> { @@ -883,6 +886,22 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, dim: bool) { pen.l(8.0, y, 20.0, y); } } + PickAdd => { + // An object box with a plus: picks add to the selection. + pen.rect(3.0, 3.0, 11.0, 11.0, b); + pen.la(17.0, 11.0, 17.0, 21.0); + pen.la(12.0, 16.0, 22.0, 16.0); + } + SelectObjects => { + // A pointer picking an object box. + pen.rect(12.0, 3.0, 9.0, 9.0, a); + pen.poly(&[(4.0, 6.0), (4.0, 20.0), (7.5, 16.5), (10.5, 21.5), (12.5, 20.5), (9.5, 15.5), (14.0, 15.0)], true, b); + } + QuickSelect => { + // A filter funnel with a spark. + pen.poly(&[(3.0, 4.0), (19.0, 4.0), (13.0, 11.0), (13.0, 19.0), (9.0, 17.0), (9.0, 11.0)], true, b); + pen.poly(&[(20.0, 10.0), (16.5, 15.5), (20.0, 15.5), (17.0, 21.0)], false, a); + } } } diff --git a/crates/ui-egui/src/palettes.rs b/crates/ui-egui/src/palettes.rs index df4c95904..347329fff 100644 --- a/crates/ui-egui/src/palettes.rs +++ b/crates/ui-egui/src/palettes.rs @@ -442,9 +442,12 @@ fn value_box(ui: &mut egui::Ui, text: &str, combo: bool, enabled: bool) -> egui: fn edit_field(ui: &mut egui::Ui, id: egui::Id, value: String) -> Option { let mut buf = ui.data_mut(|d| d.get_temp::(id)).unwrap_or_else(|| value.clone()); let w = ui.available_width() - 10.0; - let resp = ui.add_sized([w, 20.0], egui::TextEdit::singleline(&mut buf).id(id.with("te")).font(crate::theme::body())); + let te = id.with("te"); + let resp = ui.add_sized([w, 20.0], egui::TextEdit::singleline(&mut buf).id(te).font(crate::theme::body())); let mut out = None; - if resp.has_focus() { + // Keyboard focus from memory: `Response::has_focus` is false while the window itself is + // unfocused, which dropped the typed text every frame. + if ui.memory(|m| m.has_focus(te)) { ui.data_mut(|d| d.insert_temp(id, buf.clone())); } else { ui.data_mut(|d| d.remove::(id)); @@ -455,107 +458,215 @@ fn edit_field(ui: &mut egui::Ui, id: egui::Id, value: String) -> Option out } -fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { +/// A drop-down of `items` showing `current`; returns the item clicked. +fn pick_menu(ui: &mut egui::Ui, current: &str, items: impl IntoIterator) -> Option { + let mut out = None; + ui.menu_button(current.to_string(), |ui| { + for it in items { + if ui.button(&it).clicked() { + out = Some(it); + ui.close(); + } + } + }); + out +} + +/// A greyed value with no command behind it yet: visibly inert, never a silent dead button. +fn unavailable(ui: &mut egui::Ui, text: &str) { + value_box(ui, text, false, false).on_hover_text("Not available yet"); +} + +/// A greyed value computed from the geometry. +fn read_only(ui: &mut egui::Ui, text: &str) { + value_box(ui, text, false, false).on_hover_text("Read-only"); +} + +/// The Properties header: All/My, the selection-type filter and the PICKADD / Select Objects / +/// Quick Select buttons. +fn properties_header(app: &mut CadApp, ui: &mut egui::Ui) { let t = Tokens::get(); let hr = section_header(ui, "Properties"); - // All | My segmented control. + // All | My segmented control ("My" property sets don't exist yet). let seg = Rect::from_min_size(pos2(hr.right() - 120.0, hr.top() + 4.0), vec2(70.0, 18.0)); ui.painter().rect_filled(seg, 3.0, t.chrome_dark); let half = Rect::from_min_size(seg.min, vec2(35.0, 18.0)); - ui.painter().rect_filled(if app.ui.properties_all { half } else { half.translate(vec2(35.0, 0.0)) }, 3.0, t.control); + let my = half.translate(vec2(35.0, 0.0)); + ui.painter().rect_filled(if app.ui.properties_all { half } else { my }, 3.0, t.control); ui.painter().text(half.center(), egui::Align2::CENTER_CENTER, "All", crate::theme::small(), t.text); - ui.painter().text(half.center() + vec2(35.0, 0.0), egui::Align2::CENTER_CENTER, "My", crate::theme::small(), t.text); + ui.painter().text(my.center(), egui::Align2::CENTER_CENTER, "My", crate::theme::small(), t.text_faint); + ui.interact(my, ui.id().with("prop_my"), Sense::hover()).on_hover_text("My properties: not available yet"); let sel = app.session.selection(); + let pickadd = app.session.settings.pickadd; let Ok(d) = app.session.doc() else { return }; - // Selection type combo. - let types: Vec<&'static str> = sel.iter().filter_map(|h| d.entity(*h).map(|e| e.kind.type_name())).collect(); - let header = match types.len() { - 0 => "No selection".to_string(), - 1 => types.first().map(|s| s.to_string()).unwrap_or_default(), - n => { - let first = types.first().copied().unwrap_or(""); - if types.iter().all(|t| *t == first) { format!("{first} ({n})") } else { format!("All ({n})") } + // Selected handles by type, in first-seen order. + let mut types: Vec<(&'static str, Vec)> = Vec::new(); + for h in &sel { + if let Some(e) = d.entity(*h) { + let n = e.kind.type_name(); + match types.iter_mut().find(|(t, _)| *t == n) { + Some((_, hs)) => hs.push(h.hex()), + None => types.push((n, vec![h.hex()])), + } } + } + let count: usize = types.iter().map(|(_, hs)| hs.len()).sum(); + let header = match (count, types.as_slice()) { + (0, _) => "No selection".to_string(), + (1, [(n, _)]) => n.to_string(), + (n, [(ty, _)]) => format!("{ty} ({n})"), + (n, _) => format!("All ({n})"), }; + let mut run: Option<(&str, Value)> = None; + let mut start: Option<&str> = None; ui.horizontal(|ui| { ui.add_space(8.0); - value_box(ui, &header, true, !sel.is_empty()); - }); - ui.add_space(4.0); - let h = &d.header; - if sel.is_empty() { - let rows: Vec<(&str, String, bool)> = vec![ - ("Color", Color::from_aci(h.i64("CECOLOR", 256) as i16).name(), true), - ("Layer", h.str("CLAYER", "0"), true), - ("Linetype", h.str("CELTYPE", "ByLayer"), true), - ("Linetype scale", format!("{:.4}", h.f64("CELTSCALE", 1.0)), true), - ("Lineweight", cadcraft_doc::Lineweight::from_dxf(h.i64("CELWEIGHT", -1) as i16).name(), true), - ("Transparency", "ByLayer".into(), true), - ("Thickness", format!("{:.4}", h.f64("THICKNESS", 0.0)), true), - ("Text style", h.str("TEXTSTYLE", "Standard"), true), - ("Dimension style", h.str("DIMSTYLE", "Standard"), true), - ("Multileader style", h.str("CMLEADERSTYLE", "Standard"), true), - ("Table style", h.str("CTABLESTYLE", "Standard"), true), - ("Annotation scale", "1:1".into(), true), - ("Text height", format!("{:.4}", h.f64("TEXTSIZE", 0.2)), true), - ("Plot style", "ByColor".into(), true), - ("Plot style table", "None".into(), true), - ("Plot style attached to", "Model".into(), false), - ("Plot table type", "Not available".into(), false), - ]; - let layers: Vec = d.layers.iter().map(|l| l.name.clone()).collect(); - let mut action: Option<(&str, Value)> = None; - for (label, val, en) in rows { - prop_row(ui, label, |ui| match label { - "Color" => { - ui.menu_button(format!("■ {val}"), |ui| { - for (n, c) in [ - ("ByLayer", 256), - ("ByBlock", 0), - ("Red", 1), - ("Yellow", 2), - ("Green", 3), - ("Cyan", 4), - ("Blue", 5), - ("Magenta", 6), - ("White", 7), - ] { - if ui.button(n).clicked() { - action = Some(("color", json!({ "color": c }))); - ui.close(); - } - } - }); - } - "Layer" => { - ui.menu_button(val.clone(), |ui| { - for l in &layers { - if ui.button(l).clicked() { - action = Some(("layer.current", json!({ "name": l }))); - ui.close(); - } - } - }); - } - "Text height" => { - if let Some(v) = edit_field(ui, ui.id().with("cur_textsize"), val.clone()) - && let Ok(f) = v.trim().parse::() - { - action = Some(("setvar", json!({ "name": "TEXTSIZE", "value": f }))); + ui.spacing_mut().item_spacing = vec2(3.0, 0.0); + let w = (ui.available_width() - 80.0).max(60.0); + let resp = ui.allocate_ui(vec2(w, 20.0), |ui| value_box(ui, &header, true, count > 0)).inner; + if count > 0 { + // Narrow the selection to one object type. + egui::Popup::from_toggle_button_response(&resp).width(w - 10.0).close_behavior(egui::PopupCloseBehavior::CloseOnClick).show(|ui| { + for (ty, hs) in &types { + if ui.button(format!("{ty} ({})", hs.len())).clicked() { + run = Some(("select", json!({ "handles": hs }))); } } - "Linetype scale" => { - if let Some(v) = edit_field(ui, ui.id().with("cur_celtscale"), val.clone()) - && let Ok(f) = v.trim().parse::() - { - action = Some(("setvar", json!({ "name": "CELTSCALE", "value": f }))); - } - } - _ => { - value_box(ui, &val, en, en); - } }); } + let tip = if pickadd { "Toggle PICKADD (on: picks add to the selection)" } else { "Toggle PICKADD (off: each pick replaces the selection)" }; + if icons::button(ui, Icon::PickAdd, 22.0, tip, pickadd).clicked() { + run = Some(("setvar", json!({ "name": "PICKADD", "value": i32::from(!pickadd) }))); + } + if icons::button(ui, Icon::SelectObjects, 22.0, "Select Objects", false).clicked() { + start = Some("select"); + } + if icons::button(ui, Icon::QuickSelect, 22.0, "Quick Select", false).clicked() { + start = Some("ui.dialog.qselect"); + } + }); + if let Some((c, p)) = run { + let _ = app.run(c, p); + } + if let Some(c) = start { + app.start(c); + } +} + +fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { + let t = Tokens::get(); + properties_header(app, ui); + let sel = app.session.selection(); + let Ok(d) = app.session.doc() else { return }; + ui.add_space(4.0); + let h = &d.header; + if sel.is_empty() { + // Current properties for new objects; each row runs the command that sets it. + let layers: Vec = d.layers.iter().map(|l| l.name.clone()).collect(); + let linetypes: Vec = d.linetypes.iter().map(|l| l.name.clone()).collect(); + let text_styles: Vec = d.text_styles.iter().map(|l| l.name.clone()).collect(); + let dim_styles: Vec = d.dim_styles.iter().map(|l| l.name.clone()).collect(); + let ml_styles: Vec = d.mleader_styles.iter().map(|l| l.name.clone()).collect(); + let tb_styles: Vec = d.table_styles.iter().map(|l| l.name.clone()).collect(); + let mut action: Option<(&str, Value)> = None; + let mut act = |c: &'static str, p: Value| action = Some((c, p)); + prop_row(ui, "Color", |ui| { + ui.menu_button(format!("■ {}", Color::from_aci(h.i64("CECOLOR", 256) as i16).name()), |ui| { + for (n, c) in [ + ("ByLayer", 256), + ("ByBlock", 0), + ("Red", 1), + ("Yellow", 2), + ("Green", 3), + ("Cyan", 4), + ("Blue", 5), + ("Magenta", 6), + ("White", 7), + ] { + if ui.button(n).clicked() { + act("color", json!({ "color": c })); + ui.close(); + } + } + }); + }); + prop_row(ui, "Layer", |ui| { + if let Some(l) = pick_menu(ui, &h.str("CLAYER", "0"), layers) { + act("layer.current", json!({ "name": l })); + } + }); + prop_row(ui, "Linetype", |ui| { + let items = ["ByLayer", "ByBlock"] + .map(String::from) + .into_iter() + .chain(linetypes.into_iter().filter(|l| !["ByLayer", "ByBlock"].contains(&l.as_str()))); + if let Some(l) = pick_menu(ui, &h.str("CELTYPE", "ByLayer"), items) { + act("linetype", json!({ "current": l })); + } + }); + prop_row(ui, "Linetype scale", |ui| { + if let Some(v) = edit_field(ui, ui.id().with("cur_celtscale"), format!("{:.4}", h.f64("CELTSCALE", 1.0))) + && let Ok(f) = v.trim().parse::() + { + act("setvar", json!({ "name": "CELTSCALE", "value": f })); + } + }); + prop_row(ui, "Lineweight", |ui| { + ui.menu_button(cadcraft_doc::Lineweight::from_dxf(h.i64("CELWEIGHT", -1) as i16).name(), |ui| { + for n in ["ByLayer", "ByBlock", "Default"] { + if ui.button(n).clicked() { + act("lweight", json!({ "lineweight": n })); + ui.close(); + } + } + for v in cadcraft_doc::Lineweight::STANDARD { + if ui.button(format!("{:.2} mm", f64::from(v) / 100.0)).clicked() { + act("lweight", json!({ "lineweight": f64::from(v) / 100.0 })); + ui.close(); + } + } + }); + }); + prop_row(ui, "Transparency", |ui| unavailable(ui, "ByLayer")); + prop_row(ui, "Thickness", |ui| { + if let Some(v) = edit_field(ui, ui.id().with("cur_thickness"), format!("{:.4}", h.f64("THICKNESS", 0.0))) + && let Ok(f) = v.trim().parse::() + { + act("setvar", json!({ "name": "THICKNESS", "value": f })); + } + }); + prop_row(ui, "Text style", |ui| { + if let Some(n) = pick_menu(ui, &h.str("TEXTSTYLE", "Standard"), text_styles) { + act("style.current", json!({ "name": n })); + } + }); + prop_row(ui, "Dimension style", |ui| { + if let Some(n) = pick_menu(ui, &h.str("DIMSTYLE", "Standard"), dim_styles) { + act("dimstyle.current", json!({ "name": n })); + } + }); + prop_row(ui, "Multileader style", |ui| { + if let Some(n) = pick_menu(ui, &h.str("CMLEADERSTYLE", "Standard"), ml_styles) { + act("mleaderstyle", json!({ "name": n, "current": true })); + } + }); + prop_row(ui, "Table style", |ui| { + if let Some(n) = pick_menu(ui, &h.str("CTABLESTYLE", "Standard"), tb_styles) { + act("tablestyle", json!({ "name": n, "current": true })); + } + }); + prop_row(ui, "Annotation scale", |ui| unavailable(ui, "1:1")); + prop_row(ui, "Text height", |ui| { + if let Some(v) = edit_field(ui, ui.id().with("cur_textsize"), format!("{:.4}", h.f64("TEXTSIZE", 0.2))) + && let Ok(f) = v.trim().parse::() + { + act("setvar", json!({ "name": "TEXTSIZE", "value": f })); + } + }); + prop_row(ui, "Plot style", |ui| unavailable(ui, "ByColor")); + prop_row(ui, "Plot style table", |ui| unavailable(ui, "None")); + prop_row(ui, "Plot style attached to", |ui| read_only(ui, "Model")); + prop_row(ui, "Plot table type", |ui| read_only(ui, "Not available")); if let Some((c, p)) = action { let _ = app.run(c, p); } @@ -641,9 +752,11 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { }); prop_row(ui, "Lineweight", |ui| { ui.menu_button(lw.clone(), |ui| { - if ui.button("ByLayer").clicked() { - set = Some(json!({ "handles": ids, "lineweight": "ByLayer" })); - ui.close(); + for n in ["ByLayer", "ByBlock", "Default"] { + if ui.button(n).clicked() { + set = Some(json!({ "handles": ids, "lineweight": n })); + ui.close(); + } } for v in cadcraft_doc::Lineweight::STANDARD { if ui.button(format!("{:.2} mm", f64::from(v) / 100.0)).clicked() { @@ -694,11 +807,6 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { }; match &e.kind { EntityKind::Line(l) => { - let fixed = |ui: &mut egui::Ui, label: &str, v: f64| { - prop_row(ui, label, |ui| { - value_box(ui, &format!("{v:.4}"), false, true); - }) - }; prop_row(ui, "Start X", |ui| { if let Some(s) = edit_field(ui, ui.id().with(("sx", &ids)), format!("{:.4}", l.a.x)) && let Ok(f) = s.trim().parse::() @@ -728,22 +836,19 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { } }); let dlt = l.b.xy() - l.a.xy(); - fixed(ui, "Delta X", dlt.x); - fixed(ui, "Delta Y", dlt.y); - fixed(ui, "Length", dlt.len()); - fixed(ui, "Angle", dlt.angle().to_degrees()); + prop_row(ui, "Delta X", |ui| read_only(ui, &format!("{:.4}", dlt.x))); + prop_row(ui, "Delta Y", |ui| read_only(ui, &format!("{:.4}", dlt.y))); + // Length and angle keep the start point and move the end point. + num(ui, "Length", "length", dlt.len(), &mut set, &ids); + num(ui, "Angle", "angle", dlt.angle().to_degrees(), &mut set, &ids); } EntityKind::Circle(c) => { num(ui, "Center X", "cx", c.center.x, &mut set, &ids); num(ui, "Center Y", "cy", c.center.y, &mut set, &ids); num(ui, "Radius", "radius", c.radius, &mut set, &ids); num(ui, "Diameter", "diameter", c.radius * 2.0, &mut set, &ids); - prop_row(ui, "Circumference", |ui| { - value_box(ui, &format!("{:.4}", c.radius * std::f64::consts::TAU), false, true); - }); - prop_row(ui, "Area", |ui| { - value_box(ui, &format!("{:.4}", c.radius * c.radius * std::f64::consts::PI), false, true); - }); + num(ui, "Circumference", "circumference", c.radius * std::f64::consts::TAU, &mut set, &ids); + num(ui, "Area", "area", c.radius * c.radius * std::f64::consts::PI, &mut set, &ids); if let Some(v) = &mut set && let Some(o) = v.as_object_mut() { @@ -782,13 +887,9 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { EntityKind::LwPolyline(pl) => { num(ui, "Global width", "width", pl.const_width, &mut set, &ids); let g = cadcraft_geom::Polyline { vertices: pl.vertices.clone(), closed: pl.closed }; - prop_row(ui, "Length", |ui| { - value_box(ui, &format!("{:.4}", g.len()), false, true); - }); + prop_row(ui, "Length", |ui| read_only(ui, &format!("{:.4}", g.len()))); if pl.closed { - prop_row(ui, "Area", |ui| { - value_box(ui, &format!("{:.4}", g.area().abs()), false, true); - }); + prop_row(ui, "Area", |ui| read_only(ui, &format!("{:.4}", g.area().abs()))); } prop_row(ui, "Closed", |ui| { let mut c = pl.closed; @@ -819,13 +920,13 @@ fn properties_section(app: &mut CadApp, ui: &mut egui::Ui) { } }); prop_row(ui, "Dim style", |ui| { - value_box(ui, &dm.style, true, true); + if let Some(n) = pick_menu(ui, &dm.style, d.dim_styles.iter().map(|st| st.name.clone())) { + set = Some(json!({ "handles": ids, "dimStyle": n })); + } }); } EntityKind::Insert(i) => { - prop_row(ui, "Name", |ui| { - value_box(ui, &i.block, false, true); - }); + prop_row(ui, "Name", |ui| read_only(ui, &i.block)); num(ui, "Rotation", "rotation", i.rotation.to_degrees(), &mut set, &ids); num(ui, "Scale", "scale", i.scale.x, &mut set, &ids); }