refactor: make UI localization extensible and integrate upstream

This commit is contained in:
bherbruck committed 2026-10-11 07:52:08 -04:00
commit 6d4c031090
199 files changed
+31595 -2151

No files matched your search

+227 -71
View File
@@ -6,6 +6,10 @@
//! viewport, a double-click on a sketch edits it, and a right-click opens the item's menu
//! ([`crate::context_menu`]).
#[cfg(test)]
#[path = "occurrence_ui_tests.rs"]
mod occurrence_ui_tests;
use std::collections::BTreeSet;
use egui::{Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, pos2, vec2};
@@ -21,6 +25,7 @@ thread_local! {
/// The rows drawn this frame: (label, row, fold arrow area, open). For automation
/// (`ui.at {browser | chevron}`, `ui.browser`).
static DRAWN: std::cell::RefCell<Vec<(String, Rect, Option<Rect>, Option<bool>)>> = const { std::cell::RefCell::new(Vec::new()) };
static OCCURRENCES: std::cell::RefCell<Vec<(Vec<u64>, String, Rect, bool)>> = const { std::cell::RefCell::new(Vec::new()) };
}
/// The rows on screen: (label, row rect, fold arrow area, open).
@@ -28,6 +33,10 @@ pub fn drawn_rows() -> Vec<(String, Rect, Option<Rect>, Option<bool>)> {
DRAWN.with(|d| d.borrow().clone())
}
pub fn drawn_occurrences() -> Vec<(Vec<u64>, String, Rect, bool)> {
OCCURRENCES.with(|d| d.borrow().clone())
}
/// Browser state kept between frames.
#[derive(Default)]
pub struct TreeState {
@@ -41,6 +50,7 @@ pub struct TreeState {
pub occurrence_move: Option<OccMove>,
/// The Redefine Sketch Plane panel (sketch id).
pub redefine: Option<u64>,
pub redefine_draft: Option<crate::sketch_attachment_ui::Draft>,
/// Components picked in the tree (they have no viewport selection).
pub picked_components: Vec<u64>,
/// Canvases picked in the tree.
@@ -59,6 +69,11 @@ pub struct TreeState {
#[derive(Clone, Debug)]
pub struct OccMove {
pub occurrence: u64,
/// Exact assembly placement used for world-space movement and the pivot.
pub path: Vec<u64>,
/// Effective local placement at dialog opening, including its pre-existing pending move.
pub baseline: Option<solvecraft_engine::doc::Mat>,
pub had_pending: bool,
/// Distances along X, Y, Z (mm) and turns about X, Y, Z (degrees) from where it was.
pub translate: [f64; 3],
pub angles: [f64; 3],
@@ -70,7 +85,7 @@ pub struct OccMove {
impl OccMove {
pub fn new(occurrence: u64) -> Self {
OccMove { occurrence, translate: [0.0; 3], angles: [0.0; 3], pivot: None, drag0: None }
OccMove { occurrence, path: Vec::new(), baseline: None, had_pending: false, translate: [0.0; 3], angles: [0.0; 3], pivot: None, drag0: None }
}
}
@@ -98,6 +113,7 @@ pub fn set_sketch_visible(app: &mut SolveApp, id: u64, on: bool) {
pub fn redefine_sketch(app: &mut SolveApp, id: u64) {
app.tree.redefine = Some(id);
app.tree.redefine_draft = Some(crate::sketch_attachment_ui::Draft::new(app, id));
}
fn find_group(app: &SolveApp, id: u64) -> Option<&solvecraft_engine::doc::BrowserGroup> {
@@ -188,7 +204,7 @@ pub fn selection_group(app: &SolveApp, target: &Target) -> Option<Value> {
Target::Body { name } => {
let st = app.session.world_state();
let b = st.body(name)?;
(app.session.doc.body_component(name, b.feature), "bodies")
(b.component, "bodies")
}
Target::Sketch { id } => (app.session.doc.feature(*id)?.component, "sketches"),
_ => return None,
@@ -363,9 +379,9 @@ pub(crate) fn draw_row(ui: &mut egui::Ui, id: egui::Id, row: &Row) -> RowResp {
}
crate::scenario::publish_handle(&format!("row:{}", row.label), r.center());
let tip = match hover_on {
1 => Some(if row.fold == Some(true) { crate::i18n::tr("Collapse") } else { crate::i18n::tr("Expand") }),
2 => Some(if row.eye == Some(true) { crate::i18n::tr("Hide") } else { crate::i18n::tr("Show") }),
3 => Some(if row.radio == Some(true) { crate::i18n::tr("Active component") } else { crate::i18n::tr("Activate") }),
1 => Some(if row.fold == Some(true) { crate::i18n::tr("ui.collapse") } else { crate::i18n::tr("ui.expand") }),
2 => Some(if row.eye == Some(true) { crate::i18n::tr("ui.hide") } else { crate::i18n::tr("ui.show") }),
3 => Some(if row.radio == Some(true) { crate::i18n::tr("ui.active_component") } else { crate::i18n::tr("ui.activate") }),
_ => None,
};
let resp = match tip {
@@ -441,6 +457,13 @@ fn click_select(app: &mut SolveApp, ui: &egui::Ui, key: String, sel: Option<Sel>
}
fn sel_of_key(k: &str) -> Option<Sel> {
if let Some((_, id)) = k.rsplit_once("/s:") {
return id.parse().ok().map(|id| Sel::Feature { id });
}
if let Some(p) = k.strip_prefix("o:") {
let path: Option<Vec<u64>> = p.split('/').map(|id| id.parse().ok()).collect();
return path.filter(|p| !p.is_empty()).map(|path| Sel::Occurrence { path });
}
if let Some(b) = k.strip_prefix("b:") {
return Some(Sel::Body { name: b.to_string() });
}
@@ -476,6 +499,7 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
let t = Tokens::get();
app.tree.order.clear();
DRAWN.with(|d| d.borrow_mut().clear());
OCCURRENCES.with(|d| d.borrow_mut().clear());
let mut acts = Actions::default();
egui::Panel::left("sc_browser")
.exact_size(250.0)
@@ -502,7 +526,8 @@ pub fn browser(app: &mut SolveApp, ui: &mut egui::Ui) {
occurrence_panel(app, ui);
capture_prompt(app, ui.ctx());
canvas_panel(app, ui.ctx());
redefine_panel(app, ui.ctx());
let repair_anchor = app.viewport.rect.map(|rect| rect.right_top() + vec2(-16.0, 16.0)).unwrap_or_else(|| ui.ctx().content_rect().right_top());
crate::sketch_attachment_ui::panel(app, ui.ctx(), repair_anchor);
}
fn component_name(app: &SolveApp, id: u64) -> String {
@@ -599,9 +624,109 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize,
if id == 0 {
crate::dialogs_assembly::browser_joints(app, ui, d);
}
let children: Vec<u64> = app.session.doc.components.iter().filter(|c| c.parent == id).map(|c| c.id).collect();
for c in children {
component_rows(app, ui, c, d, acts);
let children: Vec<u64> = app.session.doc.occurrences.iter().filter(|o| o.parent == id).map(|o| o.id).collect();
for occurrence in children {
occurrence_rows(app, ui, vec![occurrence], d, acts);
}
}
fn occurrence_key(path: &[u64]) -> String {
format!("o:{}", path.iter().map(u64::to_string).collect::<Vec<_>>().join("/"))
}
/// A placement, rather than a component definition: repeated child IDs remain distinct
/// because their complete paths are the selection and row identity.
fn occurrence_rows(app: &mut SolveApp, ui: &mut egui::Ui, path: Vec<u64>, depth: usize, acts: &mut Actions) {
let Ok(placement) = app.session.occurrence_placement(&path) else { return };
let key = occurrence_key(&path);
let open = is_open(app, &key, true);
let active = app.session.active_occurrence_path.as_deref() == Some(path.as_slice())
|| (app.session.active_occurrence_path.is_none()
&& app.session.active_component == placement.component
&& app.session.doc.first_path(placement.component) == path);
let visible = app.session.occurrence_visible(&path);
let own_visible = !app.session.occurrence_hidden(&path);
let selected = app.session.selection.contains(&Sel::Occurrence { path: path.clone() });
let faded = app.session.active_occurrence_path.as_ref().is_some_and(|p| !path.starts_with(p) && !p.starts_with(&path));
let badges: &[&str] = if placement.grounded { &["pin"] } else { &[] };
app.tree.order.push(key.clone());
let r = draw_row(
ui,
ui.id().with(&key),
&Row {
depth,
fold: Some(open),
eye: Some(own_visible),
icon: "component",
label: &placement.name,
selected,
dim: faded || !visible,
radio: Some(active),
badges,
..Default::default()
},
);
if let Some(rect) = r.rect.filter(|_| r.resp.is_some()) {
OCCURRENCES.with(|d| d.borrow_mut().push((path.clone(), placement.name.clone(), rect, open)));
crate::scenario::publish_handle(&format!("row:occurrence:{}", key.trim_start_matches("o:")), pos2(rect.right() - 12.0, rect.center().y));
let x = rect.left() + 2.0 + depth as f32 * INDENT;
for (control, offset) in [("fold", 6.0), ("eye", 22.0), ("activate", 38.0)] {
crate::scenario::publish_handle(&format!("{control}:occurrence:{}", key.trim_start_matches("o:")), pos2(x + offset, rect.center().y));
}
}
if let Some(resp) = &r.resp {
let definition = app.session.doc.components.iter().find(|c| c.id == placement.component).map(|c| c.name.as_str()).unwrap_or("");
let location = path
.iter()
.filter_map(|id| app.session.doc.occurrences.iter().find(|o| o.id == *id))
.map(|o| o.name.as_str())
.collect::<Vec<_>>()
.join(" / ");
let inherited = if own_visible && !visible { "\nHidden by a parent placement. Show the parent to reveal this instance." } else { "" };
resp.clone().on_hover_text(format!(
"Component: {definition}\nPlacement: {}\nGeometry edits apply to every instance of this component.{inherited}",
location
));
}
if r.fold {
toggle(app, &key, true);
}
if r.eye {
let _ = app.run("browser.visibility", json!({"visible": app.session.occurrence_hidden(&path), "paths": [path]}));
}
if r.radio || r.double {
let _ = app.run("component.activate", json!({"component": placement.component, "occurrence": placement.occurrence, "path": path}));
}
if r.clicked {
click_select(app, ui, key.clone(), Some(Sel::Occurrence { path: path.clone() }));
}
if r.hovered {
acts.hover_bodies = app.session.occurrence_bodies(&path, true).unwrap_or_default();
}
if let Some(p) = r.secondary {
if !selected {
let _ = app.run("select.set", json!({"items": [Sel::Occurrence { path: path.clone() }]}));
}
acts.menu = Some((p, Target::Occurrence { path: path.clone() }));
}
if let Some(resp) = &r.resp
&& let Some(d) = drop_target(ui, resp, |d: &Drag| d.comp != placement.component && d.folder != "construction", DropMark::Box)
{
acts.drop = Some(DropAction::Component { to: placement.component, folder: d.folder, keys: d.keys.clone() });
}
if !open {
return;
}
for folder in ["bodies", "sketches", "construction"] {
occurrence_folder_rows(app, ui, placement.component, &path, folder, depth + 1, acts);
}
let children: Vec<u64> = app.session.doc.occurrences.iter().filter(|o| o.parent == placement.component).map(|o| o.id).collect();
if path.len() < 64 {
for occurrence in children {
let mut child = path.clone();
child.push(occurrence);
occurrence_rows(app, ui, child, depth + 1, acts);
}
}
}
@@ -999,13 +1124,13 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec<Entry> {
.state()
.bodies
.iter()
.filter(|b| doc.body_component(&b.name, b.feature) == comp)
.filter(|b| doc.model_body_component(b) == comp)
.map(|b| Entry {
key: b.name.clone(),
row_key: format!("b:{}", b.name),
label: b.name.clone(),
icon: "body",
visible: !app.session.visibility.hidden_bodies.contains(&b.name),
visible: app.session.body_visible(&b.name),
selected: sel.iter().any(|x| matches!(x, Sel::Body { name } if *name == b.name)),
color: None,
locked: app.ui.locked_bodies.contains(&b.name),
@@ -1020,14 +1145,22 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec<Entry> {
.filter(|f| f.component == comp && matches!(f.kind, FeatureKind::Sketch { .. }))
.map(|f| {
let active = app.session.active_sketch == Some(f.id);
let lost = app.session.model.result(f.id).is_some_and(|result| result.error.is_some())
&& matches!(doc.sketch_face_attachment(f.id, &app.session.model), solvecraft_engine::doc::FaceAttachmentStatus::Lost { .. });
Entry {
key: f.id.to_string(),
row_key: format!("s:{}", f.id),
label: if active { format!("{} (editing)", f.name) } else { f.name.clone() },
label: if lost {
format!("{} (attachment lost)", f.name)
} else if active {
format!("{} (editing)", f.name)
} else {
f.name.clone()
},
icon: "sketch",
visible: sketch_visible(app, f.id),
selected: active || sel.iter().any(|x| matches!(x, Sel::Feature { id } if *id == f.id)),
color: active.then_some(t.sketch_accent),
color: if lost { Some(t.error) } else { active.then_some(t.sketch_accent) },
locked: false,
sel: Some(Sel::Feature { id: f.id }),
target: Some(Target::Sketch { id: f.id }),
@@ -1083,9 +1216,9 @@ fn set_visible(app: &mut SolveApp, folder: &str, items: &[&Entry], on: bool) {
for e in items {
match folder {
"bodies" => {
app.session.visibility.hidden_bodies.retain(|b| *b != e.key);
app.session.visibility.hidden_bodies.retain(|b| !e.bodies.contains(b));
if !on {
app.session.visibility.hidden_bodies.push(e.key.clone());
app.session.visibility.hidden_bodies.extend(e.bodies.clone());
}
}
"sketches" => {
@@ -1122,13 +1255,38 @@ fn folder_static(folder: &str) -> &'static str {
/// A component's folder: its row, its groups and its items. Rows are drag sources; items,
/// groups and the folder row are drop targets (reorder, into a group, out of groups).
fn folder_rows(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, folder: &str, depth: usize, acts: &mut Actions) {
folder_rows_at(app, ui, comp, None, folder, depth, acts);
}
fn occurrence_folder_rows(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, path: &[u64], folder: &str, depth: usize, acts: &mut Actions) {
folder_rows_at(app, ui, comp, Some(path), folder, depth, acts);
}
fn folder_rows_at(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, path: Option<&[u64]>, folder: &str, depth: usize, acts: &mut Actions) {
let t = Tokens::get();
let all = entries(app, comp, folder);
let mut all = entries(app, comp, folder);
let prefix = path.map(occurrence_key).unwrap_or_else(|| format!("c{comp}"));
if let Some(path) = path {
let Ok(placement) = app.session.occurrence_placement(path) else { return };
for e in &mut all {
if folder == "bodies" {
let Some(body) = placement.bodies.iter().find(|b| b.source == e.key) else { continue };
e.sel = Some(Sel::Body { name: body.world.clone() });
e.target = Some(Target::Body { name: body.world.clone() });
e.bodies = vec![body.world.clone()];
e.visible = app.session.body_visible(&body.world);
e.selected = app.session.selection.contains(&Sel::Body { name: body.world.clone() });
e.row_key = format!("b:{}", body.world);
} else {
e.row_key = format!("{prefix}/{}", e.row_key);
}
}
}
if all.is_empty() {
return;
}
let fstatic = folder_static(folder);
let key = format!("c{comp}/{folder}");
let key = format!("{prefix}/{folder}");
if let Some(r) = app.tree.reveal.clone()
&& folder == "bodies"
&& all.iter().any(|e| e.key == r)
@@ -1175,7 +1333,7 @@ fn folder_rows(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, folder: &str, d
let (groups, loose) = arrange(app, comp, folder, all);
let loose_keys: Vec<String> = loose.iter().map(|e| e.key.clone()).collect();
for (g, items) in &groups {
let gkey = format!("g{}", g.id);
let gkey = format!("{prefix}/g{}", g.id);
let gopen = is_open(app, &gkey, true);
let gvis = items.is_empty() || items.iter().any(|e| e.visible);
let label = format!("{} ({})", g.name, items.len());
@@ -1204,12 +1362,12 @@ fn folder_rows(app: &mut SolveApp, ui: &mut egui::Ui, comp: u64, folder: &str, d
}
if gopen {
for e in items {
entry_row(app, ui, comp, fstatic, e, depth + 2, Some(g.id), &loose_keys, acts);
entry_row(app, ui, comp, path, fstatic, e, depth + 2, Some(g.id), &loose_keys, acts);
}
}
}
for e in &loose {
entry_row(app, ui, comp, fstatic, e, depth + 1, None, &loose_keys, acts);
entry_row(app, ui, comp, path, fstatic, e, depth + 1, None, &loose_keys, acts);
}
let _ = t;
}
@@ -1220,6 +1378,7 @@ fn entry_row(
app: &mut SolveApp,
ui: &mut egui::Ui,
comp: u64,
path: Option<&[u64]>,
folder: &'static str,
e: &Entry,
depth: usize,
@@ -1256,6 +1415,9 @@ fn entry_row(
}
if r.double && folder == "sketches" {
if let Ok(id) = e.key.parse() {
if let Some(path) = path {
let _ = app.run("component.activate", json!({"component": comp,"occurrence":path.last(),"path":path}));
}
app.edit_sketch(id);
}
} else if r.clicked {
@@ -1368,9 +1530,29 @@ fn apply_drop(app: &mut SolveApp, d: DropAction) {
/// Move/Copy of an occurrence: offsets and a rotation about Z, pending until Capture Position.
fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
let Some(mut m) = app.tree.occurrence_move.clone() else { return };
if !m.path.is_empty()
&& let Err(error) = app.session.occurrence_placement(&m.path)
{
app.tree.occurrence_move = None;
app.set_status(error.to_string(), true);
return;
}
if m.baseline.is_none() {
m.had_pending = app.session.pending_moves.contains_key(&m.occurrence);
m.baseline = app
.session
.pending_moves
.get(&m.occurrence)
.copied()
.or_else(|| app.session.doc.occurrences.iter().find(|o| o.id == m.occurrence).map(|o| o.transform));
}
let ctx = ui.ctx().clone();
if m.pivot.is_none() {
m.pivot = occurrence_origin(app, m.occurrence);
m.pivot = if m.path.is_empty() {
occurrence_origin(app, m.occurrence)
} else {
app.session.occurrence_placement(&m.path).ok().map(|p| solvecraft_engine::doc::apply_point(&p.world, solvecraft_engine::geom::Vec3::ZERO))
};
}
let name = app.session.doc.occurrences.iter().find(|o| o.id == m.occurrence).map(|o| o.name.clone()).unwrap_or_default();
let mut open = true;
@@ -1384,28 +1566,33 @@ fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
.resizable(false)
.collapsible(false)
.show(&ctx, |ui| {
if !m.path.is_empty() && app.session.occurrence_scope_count(&m.path).is_ok_and(|n| n > 1) {
ui.label("This child is shared. Position changes apply to every copy of its parent.");
}
egui::Grid::new("sc_occ_grid").num_columns(2).spacing([12.0, 6.0]).show(ui, |ui| {
for (k, axis) in ["X distance", "Y distance", "Z distance"].iter().enumerate() {
ui.label(*axis);
if let Some(v) = m.translate.get_mut(k) {
ui.add(egui::DragValue::new(v).speed(0.5).suffix(" mm"));
let response = ui.add(egui::DragValue::new(v).speed(0.5).suffix(" mm"));
crate::scenario::publish_handle(&format!("occurrence-move:distance:{k}"), response.rect.center());
}
ui.end_row();
}
for (k, axis) in ["X angle", "Y angle", "Z angle"].iter().enumerate() {
ui.label(*axis);
if let Some(v) = m.angles.get_mut(k) {
ui.add(egui::DragValue::new(v).speed(1.0).suffix(" deg"));
let response = ui.add(egui::DragValue::new(v).speed(1.0).suffix(" deg"));
crate::scenario::publish_handle(&format!("occurrence-move:angle:{k}"), response.rect.center());
}
ui.end_row();
}
});
ui.add_space(6.0);
ui.horizontal(|ui| {
if ui.button("Capture Position").clicked() {
if ui.button("Capture All Positions").clicked() {
action = Some("capture");
}
if ui.button("Revert").clicked() {
if ui.button("Revert All Positions").clicked() {
action = Some("revert");
}
});
@@ -1424,13 +1611,21 @@ fn occurrence_panel(app: &mut SolveApp, ui: &mut egui::Ui) {
app.tree.occurrence_move = None;
}
None if !open => {
let _ = app.run("component.revert_position", json!({}));
revert_occurrence_dialog(app, &m);
app.tree.occurrence_move = None;
}
None => app.tree.occurrence_move = Some(m),
}
}
pub(crate) fn revert_occurrence_dialog(app: &mut SolveApp, m: &OccMove) {
if m.had_pending {
let _ = app.run("occurrence.move", json!({"occurrence":m.occurrence,"base_transform":m.baseline,"capture":false}));
} else {
let _ = app.run("component.revert_position", json!({"occurrences":[m.occurrence]}));
}
}
/// The pending move: turned about X, then Y, then Z through the starting origin, then moved.
pub fn apply_occurrence_move(app: &mut SolveApp, m: &OccMove) {
use crate::gizmo;
@@ -1439,10 +1634,14 @@ pub fn apply_occurrence_move(app: &mut SolveApp, m: &OccMove) {
let r = gizmo::mul(&gizmo::mul(&gizmo::rotation(Vec3::Z, a[2]), &gizmo::rotation(Vec3::Y, a[1])), &gizmo::rotation(Vec3::X, a[0]));
let (axis, angle) = gizmo::axis_angle(&r);
let o = m.pivot.unwrap_or(Vec3::ZERO);
let _ = app.session.execute("component.revert_position", &json!({}));
let baseline = m.baseline.or_else(|| app.session.doc.occurrences.iter().find(|o| o.id == m.occurrence).map(|o| o.transform));
let _ = app.run(
"occurrence.move",
json!({ "occurrence": m.occurrence, "translate": m.translate, "axis": [axis.x, axis.y, axis.z], "angle": angle, "origin": [o.x, o.y, o.z], "capture": false }),
if m.path.is_empty() {
json!({ "occurrence": m.occurrence, "base_transform": baseline, "translate": m.translate, "axis": [axis.x, axis.y, axis.z], "angle": angle, "origin": [o.x, o.y, o.z], "capture": false })
} else {
json!({ "occurrence": m.occurrence, "path": m.path, "space": "world", "base_transform": baseline, "translate": m.translate, "axis": [axis.x, axis.y, axis.z], "angle": angle, "origin": [o.x, o.y, o.z], "capture": false })
},
);
}
@@ -1618,46 +1817,3 @@ fn occurrence_origin(app: &SolveApp, occurrence: u64) -> Option<solvecraft_engin
let own = app.session.pending_moves.get(&occurrence).copied().unwrap_or(o.transform);
Some(solvecraft_engine::doc::apply_point(&solvecraft_engine::doc::mat_mul(&parent, &own), solvecraft_engine::geom::Vec3::ZERO))
}
/// Redefine Sketch Plane: pick an origin or construction plane, or the selected planar face.
fn redefine_panel(app: &mut SolveApp, ctx: &egui::Context) {
let Some(id) = app.tree.redefine else { return };
let name = app.session.doc.feature(id).map(|f| f.name.clone()).unwrap_or_default();
let mut planes: Vec<String> = vec!["XY".into(), "XZ".into(), "YZ".into()];
planes.extend(app.session.doc.features.iter().filter(|f| matches!(f.kind, FeatureKind::ConstructionPlane { .. })).map(|f| f.name.clone()));
let face = app.session.selection.iter().find_map(|s| if let Sel::Face { point, .. } = s { Some(*point) } else { None });
let mut open = true;
let mut pick: Option<Value> = None;
crate::frame::window(ctx, format!("Redefine Sketch Plane: {name}"), crate::frame::Width::Normal)
.id(egui::Id::new("sc_redefine"))
.pivot(egui::Align2::LEFT_TOP)
.default_pos(panel_pos(app))
.open(&mut open)
.resizable(false)
.collapsible(false)
.show(ctx, |ui| {
ui.label(RichText::new("Move the sketch to").color(Tokens::get().text_dim));
ui.horizontal_wrapped(|ui| {
for p in &planes {
if ui.button(p).clicked() {
pick = Some(json!(p));
}
}
});
ui.add_space(4.0);
let b = ui.add_enabled(face.is_some(), egui::Button::new("Selected face"));
if b.clicked()
&& let Some(f) = face
{
pick = Some(json!({ "face": [f.x, f.y, f.z] }));
}
b.on_disabled_hover_text("Select a planar face in the viewport first");
});
if let Some(plane) = pick {
if app.run("sketch.redefine", json!({ "sketch": id, "plane": plane })).is_ok() {
app.tree.redefine = None;
}
} else if !open {
app.tree.redefine = None;
}
}