diff --git a/ROADMAP.md b/ROADMAP.md index b089a7a5..a41536e2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -61,6 +61,9 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings. ## Dimensioning and error reporting +- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot, + Polygon and Spline. Each open part remembers its last interactive choice per family + across sketch sessions and document-tab switches; closing the part resets these UI choices. - Inscribed, circumscribed and edge polygon tools preview their actual outline before placement, including orientation and the selected side count. - Construction (X) preserves the active sketch drawing tool, picked anchors and inline diff --git a/crates/ui-egui/src/documents.rs b/crates/ui-egui/src/documents.rs index 8d4b76fd..3c06b668 100644 --- a/crates/ui-egui/src/documents.rs +++ b/crates/ui-egui/src/documents.rs @@ -27,6 +27,7 @@ pub struct DocSlot { /// The per-design part of the UI state (what is hidden, shown or locked). #[derive(Clone, Debug, Default)] struct DocView { + sketch_choices: crate::sketch_families::Choices, hidden_profiles: Vec, shown_dims: Vec, locked_bodies: Vec, @@ -59,6 +60,7 @@ impl Documents { fn take_view(app: &mut SolveApp) -> DocView { let u = &mut app.ui; DocView { + sketch_choices: std::mem::take(&mut app.sketch_choices), hidden_profiles: std::mem::take(&mut u.hidden_profiles), shown_dims: std::mem::take(&mut u.shown_dims), locked_bodies: std::mem::take(&mut u.locked_bodies), @@ -66,6 +68,7 @@ fn take_view(app: &mut SolveApp) -> DocView { } fn put_view(app: &mut SolveApp, v: DocView) { + app.sketch_choices = v.sketch_choices; let u = &mut app.ui; u.hidden_profiles = v.hidden_profiles; u.shown_dims = v.shown_dims; @@ -148,6 +151,7 @@ pub fn adopt(app: &mut SolveApp, session: Session) { app.menu.props = None; crate::sketch_attachment_ui::clear(app); app.session = session; + app.sketch_choices = crate::sketch_families::Choices::default(); app.session.invalidate_view(rev); app.cam = Camera::default(); app.fit_view(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index ee8b2938..4b89ee56 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -98,6 +98,7 @@ mod sketch_display_ui_tests; mod sketch_drag_ui_tests; #[cfg(test)] mod sketch_edit_tests; +mod sketch_families; pub mod sketch_palette; pub(crate) mod sketch_text; pub mod sketch_tools; @@ -227,6 +228,7 @@ pub struct SolveApp { pub crashes: crash_ui::CrashUi, /// Toolbar buttons the user pinned, removed or reordered. pub toolbar_custom: toolbar_custom::ToolbarCustom, + pub sketch_choices: sketch_families::Choices, /// Preferences kept between runs, and their window. pub preferences: prefs::Prefs, pub prefs_window: prefs::PrefsWindow, @@ -300,6 +302,7 @@ impl SolveApp { issue_report: issue_report::ReportState::default(), crashes: crash_ui::CrashUi::default(), toolbar_custom: toolbar_custom::ToolbarCustom::default(), + sketch_choices: sketch_families::Choices::default(), preferences: prefs::Prefs::default(), prefs_window: prefs::PrefsWindow::default(), preview: preview::PreviewState::default(), @@ -464,6 +467,7 @@ impl SolveApp { Ok(_) => { self.session.echo(format!("{} done", command_label(id))); if matches!(id, "file.new" | "doc.open" | "doc.recover") { + self.sketch_choices = sketch_families::Choices::default(); dim_view::cancel(); self.menu.props = None; sketch_attachment_ui::clear(self); @@ -535,6 +539,9 @@ impl SolveApp { if !sketch_tools::start_hook(self, id) { return; } + if solidcraft_engine::find_command(id).is_some_and(|c| c.info(&self.session).enabled) { + self.sketch_choices.remember(id); + } if id == "parameters.change" { params_dialog::open(self); return; diff --git a/crates/ui-egui/src/sketch_families.rs b/crates/ui-egui/src/sketch_families.rs new file mode 100644 index 00000000..0802e4be --- /dev/null +++ b/crates/ui-egui/src/sketch_families.rs @@ -0,0 +1,93 @@ +//! Sketch creation families and the active part's last interactive variant. + +use std::collections::BTreeMap; + +use solidcraft_engine::CommandSpec; + +pub struct Family { + pub label: &'static str, + pub commands: &'static [&'static str], +} + +pub const FAMILIES: &[Family] = &[ + Family { label: "Line", commands: &["sketch.line", "sketch.line.midpoint"] }, + Family { label: "Rectangle", commands: &["sketch.rectangle.two_point", "sketch.rectangle.three_point", "sketch.rectangle.center"] }, + Family { + label: "Circle", + commands: &[ + "sketch.circle.center", + "sketch.circle.two_point", + "sketch.circle.three_point", + "sketch.circle.two_tangent", + "sketch.circle.three_tangent", + ], + }, + Family { label: "Arc", commands: &["sketch.arc.three_point", "sketch.arc.center_point", "sketch.arc.tangent"] }, + Family { + label: "Slot", + commands: &[ + "sketch.slot.center_to_center", + "sketch.slot.overall", + "sketch.slot.center_point", + "sketch.slot.arc_three_point", + "sketch.slot.arc_center", + ], + }, + Family { label: "Polygon", commands: &["sketch.polygon.inscribed", "sketch.polygon.circumscribed", "sketch.polygon.edge"] }, + Family { label: "Spline", commands: &["sketch.spline.fit_point", "sketch.spline.control_point", "sketch.spline.control_point_5"] }, +]; + +pub fn family(id: &str) -> Option<&'static Family> { + FAMILIES.iter().find(|f| f.commands.contains(&id)) +} + +/// UI state only, swapped with document tabs; never dirties the model or its undo history. +#[derive(Clone, Debug, Default)] +pub struct Choices(BTreeMap<&'static str, &'static str>); + +impl Choices { + pub fn remember(&mut self, id: &str) { + if let Some(f) = family(id) + && let Some(id) = f.commands.iter().find(|c| **c == id) + { + self.0.insert(f.label, id); + } + } + + pub fn selected(&self, f: &Family) -> Option<&'static CommandSpec> { + self.0.get(f.label).copied().or_else(|| f.commands.first().copied()).and_then(solidcraft_engine::find_command) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn families_are_disjoint_registered_creation_commands() { + let mut seen = std::collections::BTreeSet::new(); + for f in FAMILIES { + assert!(!f.commands.is_empty()); + for id in f.commands { + assert!(seen.insert(id), "duplicate variant {id}"); + let c = solidcraft_engine::find_command(id).unwrap(); + assert_eq!((c.tab, c.panel), ("SKETCH", "CREATE")); + } + } + } + + #[test] + fn choices_ignore_unknown_commands_and_keep_families_independent() { + let mut choices = Choices::default(); + choices.remember("sketch.rectangle.center"); + choices.remember("sketch.circle.three_point"); + choices.remember("sketch.rectangle.unknown"); + for (id, expected) in [ + ("sketch.rectangle.two_point", "sketch.rectangle.center"), + ("sketch.circle.center", "sketch.circle.three_point"), + ("sketch.line", "sketch.line"), + ] { + assert_eq!(choices.selected(family(id).unwrap()).unwrap().id, expected); + } + } +} diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 39763cbd..d7b1b956 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -562,7 +562,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { (panel, cmds, shown) }) .collect(); - let widths: Vec = lists.iter().map(|(_, _, shown)| panel_width(shown.len())).collect(); + let width_of = |panel: &str, count: usize| { + if tab == "SKETCH" && panel == "CREATE" { (count.max(1) as f32 * 68.0).max(64.0) + 8.0 } else { panel_width(count) } + }; + let widths: Vec = lists.iter().map(|(panel, _, shown)| width_of(panel, shown.len())).collect(); let fits = fit(&widths, r.right() - 40.0 - px); crate::scenario::publish_count("toolbar_collapsed", fits.iter().filter(|f| **f == Fit::Collapsed).count() as f64); crate::scenario::publish_count("toolbar_more", fits.iter().filter(|f| **f == Fit::More).count() as f64); @@ -573,16 +576,18 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { continue; } let collapsed = fit == Fit::Collapsed; - let width = if collapsed { COLLAPSED_WIDTH.min(panel_width(shown.len())) } else { panel_width(shown.len()) }; + let width = if collapsed { COLLAPSED_WIDTH.min(width_of(panel, shown.len())) } else { width_of(panel, shown.len()) }; + let sketch_create = tab == "SKETCH" && *panel == "CREATE"; + let stride = if sketch_create { 68.0 } else { 40.0 }; let enabled_panel = !cmds.is_empty() || *panel == "SELECT"; let popup_id = ui.id().with(("panel_menu", panel)); // Dropped on the panel's free space (all of a collapsed panel): the button goes to // its end. let drawn = if collapsed { 0 } else { shown.len() }; // Buttons narrower than the panel's minimum width (a one-button panel) are centred. - let inset = if drawn == 0 { 0.0 } else { ((width - 8.0 - (drawn as f32 * 40.0 - 2.0)) / 2.0).max(0.0).floor() }; + let inset = if drawn == 0 { 0.0 } else { ((width - 8.0 - (drawn as f32 * stride - 2.0)) / 2.0).max(0.0).floor() }; let bx = px + 4.0 + inset; - let free_x = bx + drawn as f32 * 40.0; + let free_x = bx + drawn as f32 * stride; let free = Rect::from_min_size(pos2(free_x, top + 2.0), vec2((px + width - 4.0 - free_x).max(8.0), 38.0)); let fresp = ui.interact(free, ui.id().with(("panel_free", panel)), if collapsed { Sense::click() } else { Sense::hover() }); let mut opened = false; @@ -606,12 +611,23 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { } } for (i, c) in shown.iter().enumerate().take(drawn) { - let br = Rect::from_min_size(pos2(bx + i as f32 * 40.0, top + 2.0), vec2(38.0, 38.0)); - let info = c.info(&app.session); + // Only the family's default pin follows the part's choice. Explicitly pinned + // variants remain exact commands, including in customised toolbars. + let family = crate::sketch_families::family(c.id).filter(|f| sketch_create && f.commands.first().copied() == Some(c.id)); + let selected = family.and_then(|f| app.sketch_choices.selected(f)).unwrap_or(c); + let whole = Rect::from_min_size(pos2(bx + i as f32 * stride, top + 2.0), vec2(stride - 2.0, 38.0)); + let mut br = whole; + if family.is_some() { + br.max.y -= 12.0; + } + let info = selected.info(&app.session); let resp = ui.interact(br, ui.id().with(("cmd", c.id)), Sense::click_and_drag()); crate::scenario::publish_handle(&format!("toolbar:{}", c.id), br.center()); // The button itself (`toolbar:` is also a drop-down row's while one is open). crate::scenario::publish_handle(&format!("button:{}", c.id), br.center()); + if selected.id != c.id { + crate::scenario::publish_handle(&format!("button:{}", selected.id), br.center()); + } if resp.drag_started() { resp.dnd_set_drag_payload(ToolbarDrag { id: c.id, from_menu: false }); } @@ -629,34 +645,60 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { if resp.secondary_clicked() { menu = Some((resp.interact_pointer_pos().unwrap_or(br.left_bottom()), c.id)); } - if running.as_deref() == Some(c.id) { + if running.as_deref() == Some(selected.id) { painter.rect_filled(br, 4.0, t.border.gamma_multiply(0.7)); } else if resp.hovered() && info.enabled { painter.rect_filled(br, 4.0, t.hover); } let (ink, fill, acc) = if info.enabled { (t.icon, t.icon_fill, panel_accent(t, &tab, panel)) } else { (t.border, t.panel_header, t.border) }; - let icon = match c.id { + let icon = match selected.id { "sketch.finish" => "finish", "config.table" => "configuration_table", - _ => c.icon, + _ => selected.icon, }; - icons::paint(&painter, br.shrink(6.0), icon, ink, fill, acc); + let icon_rect = + if sketch_create { Rect::from_center_size(pos2(br.center().x, br.top() + 13.0), vec2(24.0, 24.0)) } else { br.shrink(6.0) }; + icons::paint(&painter, icon_rect, icon, ink, fill, acc); let label = match c.id { "parameters.change" if *panel == "CONFIGURE" => "Parameters", "config.table" if *panel == "CONFIGURE" => "Configurations", - _ => c.label, + _ => selected.label, }; let tr_label = app.tr_label(label); resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, ui.is_enabled() && info.enabled, tr_label)); let tip = match &info.disabled_reason { Some(why) => format!("{}\n{why}", tr_label), - None => format!("{}{}", tr_label, c.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()), + None => format!("{}{}", tr_label, selected.shortcut.map(|k| format!(" ({k})")).unwrap_or_default()), }; if resp.on_hover_text(tip).clicked() && info.enabled { - let id = c.id; + let id = selected.id; app.start(id); } + if let Some(f) = family { + let ar = Rect::from_min_max(pos2(whole.left(), br.bottom()), whole.right_bottom()); + let arrow = ui.interact(ar, ui.id().with(("family", f.label)), Sense::click()); + crate::scenario::publish_handle(&format!("family:{}", f.label), ar.center()); + if arrow.hovered() { + painter.rect_filled(ar, 3.0, t.hover); + } + painter.text(ar.center() - vec2(4.0, 0.0), Align2::CENTER_CENTER, app.tr_label(f.label), FontId::proportional(10.0), t.text); + caret(&painter, pos2(ar.right() - 5.0, ar.center().y), t.text); + arrow.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, format!("{} variants", f.label))); + egui::Popup::menu(&arrow).show(|ui| { + let variants: Vec<_> = f.commands.iter().filter_map(|id| solidcraft_engine::find_command(id)).collect(); + dropdown_keys(app, ui, &variants); + sketch_menu_rows(app, ui, &variants, &mut menu, &mut toggle); + }); + } else if sketch_create { + painter.text( + pos2(br.center().x, br.bottom() - 5.0), + Align2::CENTER_CENTER, + app.tr_label(c.label), + FontId::proportional(10.0), + t.text, + ); + } } if shown.is_empty() && !collapsed { let br = Rect::from_min_size(pos2(px + 4.0, top + 2.0), vec2(width - 8.0, 38.0)); @@ -698,9 +740,9 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { // Labels, gaps and margins append; actual buttons keep their insert-before target. // Respect the current layer so a dropdown covering the panel is not a drop target. let panel_rect = Rect::from_min_max(pos2(px, top + 2.0), pos2(px + width, r.bottom())); - let over_button = ui - .input(|input| input.pointer.hover_pos()) - .is_some_and(|p| (0..drawn).any(|i| Rect::from_min_size(pos2(bx + i as f32 * 40.0, top + 2.0), vec2(38.0, 38.0)).contains(p))); + let over_button = ui.input(|input| input.pointer.hover_pos()).is_some_and(|p| { + (0..drawn).any(|i| Rect::from_min_size(pos2(bx + i as f32 * stride, top + 2.0), vec2(stride - 2.0, 38.0)).contains(p)) + }); if !over_button && ui.rect_contains_pointer(panel_rect) && egui::DragAndDrop::payload::(ui.ctx()).is_some() { painter.line_segment([pos2(px + width - 4.0, top + 4.0), pos2(px + width - 4.0, top + 38.0)], Stroke::new(2.0, t.accent)); if ui.input(|input| input.pointer.any_released()) @@ -720,6 +762,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { } else if cmds.is_empty() { ui.label(egui::RichText::new("Not available yet").color(t.text_dim)); } + if sketch_create { + sketch_create_menu(app, ui, &cmds, &mut menu, &mut toggle); + return; + } let row_w = dropdown_width(app, ui, &cmds); // Up and Down move the keyboard focus through the enabled rows (from none // focused: the first or the last), instead of egui's own spatial move. @@ -778,6 +824,10 @@ pub fn toolbar(app: &mut SolveApp, ui: &mut egui::Ui) { let response = ui.menu_button("ยป", |ui| { for (panel, cmds) in &overflow { ui.menu_button(*panel, |ui| { + if tab == "SKETCH" && *panel == "CREATE" { + sketch_create_menu(app, ui, cmds, &mut menu, &mut toggle); + return; + } if *panel == "SELECT" { selection_filter(app, ui); } @@ -924,6 +974,91 @@ fn dropdown_row(app: &SolveApp, ui: &mut egui::Ui, c: &'static solidcraft_engine RowAction::None } +/// Creation menus keep related variants together, including collapsed and overflow panels. +fn sketch_create_menu( + app: &mut SolveApp, + ui: &mut egui::Ui, + cmds: &[&'static solidcraft_engine::CommandSpec], + menu: &mut Option<(egui::Pos2, &'static str)>, + toggle: &mut Option<&'static str>, +) { + let room = (ui.ctx().content_rect().bottom() - ui.cursor().top() - 12.0).max(ROW_H * 3.0); + egui::ScrollArea::vertical().id_salt("sketch_create_rows").max_height(room).show(ui, |ui| { + for f in crate::sketch_families::FAMILIES { + let variants: Vec<_> = f.commands.iter().filter_map(|id| cmds.iter().find(|c| c.id == *id).copied()).collect(); + if !variants.is_empty() { + let response = ui.menu_button(app.tr_label(f.label).to_string(), |ui| { + dropdown_keys(app, ui, &variants); + sketch_menu_rows(app, ui, &variants, menu, toggle); + }); + crate::scenario::publish_handle(&format!("create_family:{}", f.label), response.response.rect.center()); + } + } + ui.separator(); + let rest: Vec<_> = cmds.iter().copied().filter(|c| crate::sketch_families::family(c.id).is_none()).collect(); + dropdown_keys(app, ui, &rest); + sketch_menu_rows(app, ui, &rest, menu, toggle); + }); +} + +/// Family popups use the same ordered arrow-key navigation as a panel's command list. +fn dropdown_keys(app: &SolveApp, ui: &egui::Ui, cmds: &[&'static solidcraft_engine::CommandSpec]) { + let (down, up) = ui.input(|i| (i.key_pressed(egui::Key::ArrowDown), i.key_pressed(egui::Key::ArrowUp))); + if !down && !up { + return; + } + // A submenu renders before the remaining rows of its parent. Handle a key once, but + // leave the event available for the focused row's scroll-into-view behaviour. + let handled = egui::Id::new("sketch_menu_key_pass"); + let pass = ui.ctx().cumulative_pass_nr(); + if ui.ctx().data(|d| d.get_temp::(handled)) == Some(pass) { + return; + } + ui.ctx().data_mut(|d| d.insert_temp(handled, pass)); + let enabled: Vec<_> = cmds.iter().filter(|c| c.info(&app.session).enabled).collect(); + if enabled.is_empty() { + return; + } + let focused = ui.ctx().memory(|m| m.focused()); + let at = enabled.iter().position(|c| focused == Some(dropdown_row_id(c.id))); + let n = enabled.len(); + let next = match (at, down) { + (None, true) => 0, + (None, false) => n - 1, + (Some(i), true) => (i + 1) % n, + (Some(i), false) => (i + n - 1) % n, + }; + if let Some(c) = enabled.get(next) { + ui.ctx().memory_mut(|m| { + m.request_focus(dropdown_row_id(c.id)); + m.move_focus(egui::FocusDirection::None); + }); + } +} + +fn sketch_menu_rows( + app: &mut SolveApp, + ui: &mut egui::Ui, + cmds: &[&'static solidcraft_engine::CommandSpec], + menu: &mut Option<(egui::Pos2, &'static str)>, + toggle: &mut Option<&'static str>, +) { + let width = dropdown_width(app, ui, cmds); + for c in cmds { + let pinned = crate::toolbar_custom::pinned_on(app, "SKETCH", c.id).is_some(); + match dropdown_row(app, ui, c, width, pinned) { + RowAction::None => {} + RowAction::Menu(at) => *menu = Some((at, c.id)), + RowAction::TogglePin => *toggle = Some(c.id), + RowAction::Start => { + app.start(c.id); + ui.ctx().data_mut(|d| d.remove::(egui::Id::new("sc_panel_open"))); + ui.close(); + } + } + } +} + /// A command being dragged: a toolbar button, or a row of a panel drop-down (`from_menu`). #[derive(Clone, Copy, Debug)] struct ToolbarDrag { diff --git a/crates/ui-egui/src/workspace.rs b/crates/ui-egui/src/workspace.rs index 5ab66a4e..5ff07110 100644 --- a/crates/ui-egui/src/workspace.rs +++ b/crates/ui-egui/src/workspace.rs @@ -104,13 +104,14 @@ pub fn layout<'a>(tab: &str, panel: &str, specs: &[&'a CommandSpec]) -> Option<( &[ "sketch.line", "sketch.rectangle.two_point", - "sketch.rectangle.three_point", - "sketch.rectangle.center", "sketch.circle.center", - "sketch.circle.two_point", + "sketch.arc.three_point", + "sketch.slot.center_to_center", + "sketch.polygon.inscribed", + "sketch.spline.fit_point", "sketch.point", ], - 7, + 8, tab, ), ("SOLID", "CONFIGURE") => (&["parameters.change", "config.table"], 2, tab), diff --git a/crates/ui-egui/tests/scenarios/s49_sketch_point_toolbar.json b/crates/ui-egui/tests/scenarios/s49_sketch_point_toolbar.json index 4016041a..4dcf2f73 100644 --- a/crates/ui-egui/tests/scenarios/s49_sketch_point_toolbar.json +++ b/crates/ui-egui/tests/scenarios/s49_sketch_point_toolbar.json @@ -1,7 +1,7 @@ [ - {"note":"#191: Point is a default Sketch CREATE button; the existing six drawing buttons remain available."}, + {"note":"#191: Point remains a default Sketch CREATE button alongside the drawing families."}, {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, - {"expect":{"shown_handles":["button:sketch.line","button:sketch.rectangle.two_point","button:sketch.rectangle.three_point","button:sketch.rectangle.center","button:sketch.circle.center","button:sketch.circle.two_point","button:sketch.point"]}}, + {"expect":{"shown_handles":["button:sketch.line","family:Rectangle","family:Circle","family:Arc","family:Slot","family:Polygon","family:Spline","button:sketch.point"]}}, {"click":{"handle":"button:sketch.point"}}, {"expect":{"tool":"sketch.point","dialog":null}}, {"click":{"sketch":[20,15]}}, diff --git a/crates/ui-egui/tests/scenarios/s51_sketch_families.json b/crates/ui-egui/tests/scenarios/s51_sketch_families.json new file mode 100644 index 00000000..b4878f93 --- /dev/null +++ b/crates/ui-egui/tests/scenarios/s51_sketch_families.json @@ -0,0 +1,41 @@ +[ + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"click":{"handle":"family:Rectangle"}}, + {"expect":{"shown_handles":["dropdown:sketch.rectangle.two_point","dropdown:sketch.rectangle.three_point","dropdown:sketch.rectangle.center"],"hidden_handles":["dropdown:sketch.circle.center"]}}, + {"key":"ArrowDown"}, + {"expect":{"shown_handles":["dropdown_hot:sketch.rectangle.two_point"]}}, + {"key":"ArrowUp"}, + {"expect":{"shown_handles":["dropdown_hot:sketch.rectangle.center"]}}, + {"key":"Enter"}, + {"expect":{"tool":"sketch.rectangle.center"}}, + {"click":{"sketch":[0,0]}}, + {"click":{"sketch":[20,15]}}, + {"key":"Escape"}, + {"click":{"handle":"button:sketch.rectangle.two_point"}}, + {"expect":{"tool":"sketch.rectangle.center"}}, + {"key":"Escape"}, + {"call":"ui.documents","params":{"action":"new"}}, + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"click":{"handle":"button:sketch.rectangle.two_point"}}, + {"expect":{"tool":"sketch.rectangle.two_point"}}, + {"key":"Escape"}, + {"call":"ui.documents","params":{"action":"switch","index":0}}, + {"click":{"handle":"button:sketch.rectangle.two_point"}}, + {"expect":{"tool":"sketch.rectangle.center"}}, + {"key":"Escape"}, + {"start":"sketch.finish"}, + {"call":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}, + {"click":{"handle":"button:sketch.rectangle.two_point"}}, + {"expect":{"tool":"sketch.rectangle.center"}}, + {"key":"Escape"}, + {"call":"ui.resize","params":{"width":640,"height":720}}, + {"click":{"handle":"panel:SKETCH:CREATE"}}, + {"expect":{"shown_handles":["create_family:Rectangle","create_family:Circle"],"hidden_handles":["dropdown:sketch.rectangle.center"]}}, + {"click":{"handle":"create_family:Rectangle"}}, + {"click":{"handle":"dropdown:sketch.rectangle.three_point"}}, + {"expect":{"tool":"sketch.rectangle.three_point","errors":0}}, + {"key":"Escape"}, + {"call":"ui.resize","params":{"width":1280,"height":800}}, + {"click":{"handle":"button:sketch.rectangle.two_point"}}, + {"expect":{"tool":"sketch.rectangle.three_point","errors":0}} +]