Hide and show joints in the browser, like other items (#1168)
Fixes #1160.
This commit is contained in:
1 parent
e3459edea8
commit
229de1484c
15 files changed
+470
-15
No files matched your search
@@ -1051,14 +1051,24 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
use crate::browser::{Row, draw_row, is_open, toggle};
|
||||
let joints: Vec<(u64, String, JointKind, bool)> =
|
||||
app.session.doc.assembly.joints.iter().map(|j| (j.id, j.name.clone(), j.kind, j.suppressed)).collect();
|
||||
app.tree.picked_joints.retain(|p| joints.iter().any(|j| j.0 == *p));
|
||||
if joints.is_empty() {
|
||||
return;
|
||||
}
|
||||
// Drawn like every other browser node (its fold and rows publish `fold:Joints`, `row:<name>`).
|
||||
// Drawn like every other browser node (its fold, eye and rows publish `fold:Joints`,
|
||||
// `eye:Joints`, `row:<name>`). The folder's eye hides every joint's glyph, or shows them
|
||||
// all when none is shown.
|
||||
let key = "c0/joints";
|
||||
let open = is_open(app, key, true);
|
||||
let r = draw_row(ui, ui.id().with(key), &Row { depth, fold: Some(open), icon: "folder", label: "Joints", ..Default::default() });
|
||||
if r.fold || r.clicked {
|
||||
let any_visible = joints.iter().any(|j| app.session.joint_visible(j.0));
|
||||
let r = draw_row(
|
||||
ui,
|
||||
ui.id().with(key),
|
||||
&Row { depth, fold: Some(open), eye: Some(any_visible), icon: "folder", label: "Joints", ..Default::default() },
|
||||
);
|
||||
if r.eye {
|
||||
let _ = app.run("browser.visibility", json!({ "folder": "joints", "visible": !any_visible }));
|
||||
} else if r.fold || r.clicked {
|
||||
toggle(app, key, true);
|
||||
}
|
||||
if !open {
|
||||
@@ -1066,16 +1076,29 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
}
|
||||
let mut dialog: Option<Dialog> = None;
|
||||
for (id, name, kind, suppressed) in joints {
|
||||
let r =
|
||||
draw_row(ui, ui.id().with(("joint", id)), &Row { depth: depth + 1, icon: "joint", label: &name, dim: suppressed, ..Default::default() });
|
||||
let visible = app.session.joint_visible(id);
|
||||
let selected = app.tree.picked_joints.contains(&id);
|
||||
let r = draw_row(
|
||||
ui,
|
||||
ui.id().with(("joint", id)),
|
||||
&Row { depth: depth + 1, eye: Some(visible), icon: "joint", label: &name, selected, dim: suppressed || !visible, ..Default::default() },
|
||||
);
|
||||
if let Some(resp) = &r.resp {
|
||||
resp.clone().on_hover_text(format!("{kind:?} joint"));
|
||||
}
|
||||
if r.eye {
|
||||
let _ = app.run("browser.visibility", json!({ "joints": [id], "visible": !visible }));
|
||||
} else if r.clicked {
|
||||
pick_joint(app, id, ui.input(|i| i.modifiers.command || i.modifiers.ctrl));
|
||||
}
|
||||
if r.double {
|
||||
dialog = edit_joint(app, id);
|
||||
}
|
||||
// The same menus as the rest of the browser.
|
||||
if let Some(at) = r.secondary {
|
||||
if !app.tree.picked_joints.contains(&id) {
|
||||
app.tree.picked_joints = vec![id];
|
||||
}
|
||||
crate::context_menu::open_for(app, at, crate::context_menu::Target::Joint { id });
|
||||
}
|
||||
}
|
||||
@@ -1085,6 +1108,25 @@ pub fn browser_joints(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
|
||||
}
|
||||
}
|
||||
|
||||
/// A click on a joint's browser row: it becomes the pick (or, with Ctrl, toggles in or out of
|
||||
/// it). Picked joints draw highlighted in the viewport, even hidden ones.
|
||||
pub fn pick_joint(app: &mut SolveApp, id: u64, add: bool) {
|
||||
if !add {
|
||||
let was = app.tree.picked_joints == [id];
|
||||
app.tree.picked_joints.clear();
|
||||
app.tree.picked_canvases.clear();
|
||||
app.tree.picked_components.clear();
|
||||
let _ = app.run("select.clear", json!({}));
|
||||
if !was {
|
||||
app.tree.picked_joints.push(id);
|
||||
}
|
||||
} else if app.tree.picked_joints.contains(&id) {
|
||||
app.tree.picked_joints.retain(|j| *j != id);
|
||||
} else {
|
||||
app.tree.picked_joints.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "dialogs_assembly_tests.rs"]
|
||||
mod tests;
|
||||
@@ -1136,3 +1178,118 @@ pub fn origins(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
painter.circle(c, 4.5, t.panel, Stroke::new(1.5, t.text));
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// Joint frames in the world (joint id → A's frame), for the design state they were found in.
|
||||
static JOINT_FRAMES: std::cell::RefCell<Option<(String, Vec<(u64, solidcraft_engine::doc::Mat)>)>> = const { std::cell::RefCell::new(None) };
|
||||
}
|
||||
|
||||
/// Each joint's frame in the world (where its glyph sits): the frame of its first origin, on
|
||||
/// its occurrence as placed (including moves not captured yet).
|
||||
pub fn joint_frames_world(s: &Session) -> Vec<(u64, solidcraft_engine::doc::Mat)> {
|
||||
if s.doc.assembly.joints.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let key = format!("{}:{:?}", s.revision, s.pending_moves);
|
||||
if let Some(v) = JOINT_FRAMES.with(|o| o.borrow().as_ref().filter(|(k, _)| *k == key).map(|(_, v)| v.clone())) {
|
||||
return v;
|
||||
}
|
||||
let doc: std::borrow::Cow<solidcraft_engine::doc::Document> = if s.pending_moves.is_empty() {
|
||||
std::borrow::Cow::Borrowed(&*s.doc)
|
||||
} else {
|
||||
let mut d = (*s.doc).clone();
|
||||
for o in &mut d.occurrences {
|
||||
if let Some(m) = s.pending_moves.get(&o.id) {
|
||||
o.transform = *m;
|
||||
}
|
||||
}
|
||||
std::borrow::Cow::Owned(d)
|
||||
};
|
||||
let st = s.model.state();
|
||||
let v: Vec<_> = doc.assembly.joints.iter().filter_map(|j| doc.origin_world(&st, &j.a).ok().map(|m| (j.id, m))).collect();
|
||||
JOINT_FRAMES.with(|o| *o.borrow_mut() = Some((key, v.clone())));
|
||||
v
|
||||
}
|
||||
|
||||
/// The joints whose glyph the viewport draws, and whether each is highlighted. A hidden joint
|
||||
/// isn't drawn (it still constrains and moves the parts), except while it is picked in the
|
||||
/// browser: then it shows highlighted until the pick goes away.
|
||||
pub fn drawn_joints(app: &SolveApp) -> Vec<(u64, bool)> {
|
||||
app.session
|
||||
.doc
|
||||
.assembly
|
||||
.joints
|
||||
.iter()
|
||||
.filter_map(|j| {
|
||||
let picked = app.tree.picked_joints.contains(&j.id);
|
||||
(picked || app.session.joint_visible(j.id)).then_some((j.id, picked))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Joint glyphs in the viewport: a ring where the joint sits, its motion axis, and its limits
|
||||
/// (ticks on the ring for a turn, along the axis for a slide). Glyphs are only drawn; they are
|
||||
/// never picked in the viewport.
|
||||
pub fn joint_glyphs(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
|
||||
let drawn = drawn_joints(app);
|
||||
if drawn.is_empty() {
|
||||
return;
|
||||
}
|
||||
let t = Tokens::get();
|
||||
let frames = joint_frames_world(&app.session);
|
||||
let len = app.cam.half_height() * 2.0 * 0.05;
|
||||
let at = |m: &solidcraft_engine::doc::Mat, x: f64, y: f64, z: f64| solidcraft_engine::doc::apply_point(m, Vec3::new(x, y, z));
|
||||
for (id, picked) in drawn {
|
||||
let Some(j) = app.session.doc.assembly.joints.iter().find(|j| j.id == id) else { continue };
|
||||
let Some((_, m)) = frames.iter().find(|(f, _)| *f == id) else { continue };
|
||||
let col = if picked {
|
||||
t.sel_edge
|
||||
} else if j.suppressed {
|
||||
t.text_dim
|
||||
} else {
|
||||
t.accent
|
||||
};
|
||||
let width = if picked { 2.5 } else { 1.5 };
|
||||
let stroke = Stroke::new(width, col);
|
||||
let Some(c) = proj.to_screen(at(m, 0.0, 0.0, 0.0)) else { continue };
|
||||
// The motion axis (z), both ways.
|
||||
if let (Some(a), Some(b)) = (proj.to_screen(at(m, 0.0, 0.0, -len)), proj.to_screen(at(m, 0.0, 0.0, len))) {
|
||||
painter.line_segment([a, b], stroke);
|
||||
}
|
||||
// A ring about the axis.
|
||||
let r = len * 0.5;
|
||||
let ring: Vec<Pos2> = (0..=32)
|
||||
.filter_map(|k| {
|
||||
let a = k as f64 / 32.0 * std::f64::consts::TAU;
|
||||
proj.to_screen(at(m, r * a.cos(), r * a.sin(), 0.0))
|
||||
})
|
||||
.collect();
|
||||
if ring.len() > 1 {
|
||||
painter.add(egui::Shape::line(ring, stroke));
|
||||
}
|
||||
// Limits: ticks across the ring for a turn, across the axis (or slot) for a slide.
|
||||
for (i, lim) in j.limits.iter().enumerate() {
|
||||
let Some((lo, hi)) = lim else { continue };
|
||||
let name = j.kind.dofs().get(i).copied().unwrap_or("");
|
||||
for v in [*lo, *hi] {
|
||||
if !v.is_finite() {
|
||||
continue;
|
||||
}
|
||||
let (p, q) = match name {
|
||||
"angle" => ((v.cos() * r * 0.7, v.sin() * r * 0.7, 0.0), (v.cos() * r * 1.3, v.sin() * r * 1.3, 0.0)),
|
||||
"distance" if v.abs() < 1e6 => ((-r * 0.3, 0.0, v), (r * 0.3, 0.0, v)),
|
||||
"slide" if v.abs() < 1e6 => ((v, -r * 0.3, 0.0), (v, r * 0.3, 0.0)),
|
||||
_ => continue,
|
||||
};
|
||||
if let (Some(p), Some(q)) = (proj.to_screen(at(m, p.0, p.1, p.2)), proj.to_screen(at(m, q.0, q.1, q.2))) {
|
||||
painter.line_segment([p, q], Stroke::new(width, t.warning));
|
||||
}
|
||||
}
|
||||
}
|
||||
painter.circle(c, if picked { 4.5 } else { 3.5 }, t.panel, stroke);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "joint_browser_tests.rs"]
|
||||
mod joint_browser_tests;
|
||||
Reference in new issue
Block a user