browser.visibility hides and shows sketches, bodies, construction geometry and origin items (items: ids or names, or a whole folder; visible: bool), so agents can hide helper sketches over MCP. What is hidden moved from the egui UI state into the engine session (view state: not saved, not an undo step); the desktop eye toggles and menus use it, headless renders and fit leave hidden items out, and document.inspect, sketch.inspect and ui.inspect report visibility. ui.set still accepts hiddenBodies, hiddenOrigin, hiddenSketches and shownSketches. Tests: helper sketches hide and show in the engine (hostile input changes nothing) and through MCP only

This commit is contained in:
CypherNaugh_0x authored and bherbruck committed 2026-10-10 14:33:56 -04:00
1 parent 0321778d38
commit 81be4e74b8
16 files changed
+306 -118

No files matched your search

+139 -1
View File
@@ -9,7 +9,7 @@ use serde_json::{Value, json};
use solvecraft_doc::FeatureKind;
use super::CommandSpec;
use crate::params::{bad, str_};
use crate::params::{bad, bool_, str_};
use crate::{Result, Session};
pub static COMMANDS: &[CommandSpec] = &[
@@ -34,6 +34,10 @@ pub static COMMANDS: &[CommandSpec] = &[
CommandSpec::new("view.delete", "Delete Named View", view_delete).params("view: name"),
CommandSpec::new("view.list", "List Named Views", view_list).noundo(),
CommandSpec::new("browser.order", "Order Browser Items", order).params("folder, component?, order: [keys] (the folder's items in display order)"),
CommandSpec::new("browser.visibility", "Show/Hide", visibility).icon("eye").noundo().params(
"visible: bool; items?: [sketch id or name | body name | construction name | O|X|Y|Z|XY|XZ|YZ]; \
or folder?: bodies|sketches|construction|origin (all of it). View state: not saved, not an undo step",
),
];
const FOLDERS: [&str; 3] = ["bodies", "sketches", "construction"];
@@ -155,6 +159,83 @@ fn order(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({}))
}
const ORIGIN: [&str; 7] = ["O", "X", "Y", "Z", "XY", "XZ", "YZ"];
/// A browser item whose visibility can change.
enum Shown {
Sketch(u64),
Body(String),
/// Origin items and construction geometry, by name.
Origin(String),
}
fn is_construction(k: &FeatureKind) -> bool {
matches!(k, FeatureKind::ConstructionPlane { .. } | FeatureKind::ConstructionAxis { .. } | FeatureKind::ConstructionPoint { .. })
}
fn shown_item(s: &Session, key: &str) -> Option<Shown> {
if let Some(f) = s.doc.find_feature(key) {
if matches!(f.kind, FeatureKind::Sketch { .. }) {
return Some(Shown::Sketch(f.id));
}
if is_construction(&f.kind) {
return Some(Shown::Origin(f.name.clone()));
}
}
if s.world_state().bodies.iter().any(|b| b.name == key) {
return Some(Shown::Body(key.to_string()));
}
ORIGIN.iter().find(|o| o.eq_ignore_ascii_case(key)).map(|o| Shown::Origin(o.to_string()))
}
fn visibility(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "browser.visibility";
let visible = bool_(p, "visible").ok_or_else(|| bad(cmd, "`visible` must be true or false"))?;
let items: Vec<Shown> = match (str_(p, "folder"), p.get("items")) {
(Some(_), Some(_)) => return Err(bad(cmd, "give `items` or `folder`, not both")),
(Some("bodies"), None) => s.world_state().bodies.iter().map(|b| Shown::Body(b.name.clone())).collect(),
(Some("sketches"), None) => s.model.state().sketches.iter().map(|x| Shown::Sketch(x.feature)).collect(),
(Some("construction"), None) => s.doc.features.iter().filter(|f| is_construction(&f.kind)).map(|f| Shown::Origin(f.name.clone())).collect(),
(Some("origin"), None) => ORIGIN.iter().map(|o| Shown::Origin(o.to_string())).collect(),
(Some(f), None) => return Err(bad(cmd, format!("unknown folder `{f}` (bodies, sketches, construction, origin)"))),
(None, Some(_)) => {
let keys = keys(p, "items", cmd)?;
if keys.is_empty() {
return Err(bad(cmd, "`items` must list sketches, bodies or construction geometry"));
}
// Every item is found before anything changes.
keys.iter()
.map(|k| shown_item(s, k).ok_or_else(|| bad(cmd, format!("no sketch, body or construction item `{k}`"))))
.collect::<Result<_>>()?
}
(None, None) => return Err(bad(cmd, "`items` or `folder` is required")),
};
let v = &mut s.visibility;
for it in &items {
match it {
Shown::Sketch(id) => {
v.hidden_sketches.retain(|x| x != id);
v.shown_sketches.retain(|x| x != id);
if visible { v.shown_sketches.push(*id) } else { v.hidden_sketches.push(*id) }
}
Shown::Body(n) => {
v.hidden_bodies.retain(|x| x != n);
if !visible {
v.hidden_bodies.push(n.clone());
}
}
Shown::Origin(n) => {
v.hidden_origin.retain(|x| x != n);
if !visible {
v.hidden_origin.push(n.clone());
}
}
}
}
s.revision += 1;
Ok(json!({ "visible": visible, "changed": items.len(), "visibility": s.visibility }))
}
fn document_units(s: &mut Session, p: &Value) -> Result<Value> {
let cmd = "document.units";
let u = str_(p, "units").ok_or_else(|| bad(cmd, "`units` is required"))?;
@@ -339,6 +420,9 @@ fn rename_body(s: &mut Session, p: &Value) -> Result<Value> {
d.body_components.insert(name.to_string(), c);
}
rename_key(d, "bodies", &old, name);
for b in s.visibility.hidden_bodies.iter_mut().filter(|b| **b == old) {
*b = name.to_string();
}
Ok(json!({"body": old, "name": name}))
}
@@ -478,6 +562,60 @@ mod tests {
assert_eq!(s.doc.named_views.len(), 2);
}
#[test]
fn helper_sketches_hide_and_show() {
let mut s = Session::default();
run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10}));
let floor = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.line", json!({"points": [[0, 0], [50, 0]], "construction": true}));
run(&mut s, "sketch.finish", json!({}));
let drain = run(&mut s, "sketch.create", json!({"plane": "XY"}))["sketch"].as_u64().unwrap();
run(&mut s, "sketch.point", json!({"point": [25, 5]}));
run(&mut s, "sketch.finish", json!({}));
run(&mut s, "construct.plane.offset", json!({"base": "XY", "offset": 5, "name": "Floor"}));
let body = s.model.state().bodies[0].name.clone();
let drain_name = s.doc.feature(drain).unwrap().name.clone();
let cam = solvecraft_render::Camera::default();
let lines = |s: &Session| crate::view::scene(s, &cam).lines.len();
let shown = |s: &mut Session| {
let d = run(s, "document.inspect", json!({}));
d["sketches"].as_array().unwrap().iter().filter(|x| x["visible"] == true).count()
};
let (n0, undo0, doc0) = (lines(&s), s.undo.len(), s.doc.clone());
assert_eq!(shown(&mut s), 2);
// By id and by name; the view loses their lines, the design and its history don't change.
let r = run(&mut s, "browser.visibility", json!({"items": [floor, drain_name], "visible": false}));
assert_eq!(r["changed"], 2);
assert_eq!(shown(&mut s), 0);
assert!(lines(&s) < n0);
assert!(std::sync::Arc::ptr_eq(&s.doc, &doc0) && s.undo.len() == undo0, "view state, not a design change");
run(&mut s, "browser.visibility", json!({"folder": "sketches", "visible": true}));
assert_eq!(lines(&s), n0);
// Bodies (following a rename) and construction planes.
run(&mut s, "browser.visibility", json!({"items": [body.clone(), "Floor", "xy"], "visible": false}));
assert!(crate::view::scene(&s, &cam).meshes.is_empty());
assert_eq!(s.visibility.hidden_origin, vec!["Floor".to_string(), "XY".to_string()]);
run(&mut s, "body.rename", json!({"body": body, "name": "Tray"}));
assert_eq!(s.visibility.hidden_bodies, vec!["Tray".to_string()]);
let d = run(&mut s, "document.inspect", json!({}));
assert_eq!(d["bodies"][0]["visible"], false);
// Hostile input is refused and changes nothing.
let before = s.visibility.clone();
for p in [
json!({"items": [floor]}),
json!({"items": [floor], "visible": "no"}),
json!({"items": [floor, "Nope"], "visible": false}),
json!({"items": [], "visible": false}),
json!({"items": "Floor", "visible": false}),
json!({"folder": "sketches", "items": [floor], "visible": false}),
json!({"folder": "nope", "visible": false}),
json!({"visible": false}),
] {
assert!(s.execute("browser.visibility", &p).is_err(), "{p}");
}
assert_eq!(s.visibility, before);
}
#[test]
fn groups_hold_items_and_follow_renames() {
let mut s = Session::default();
+14 -4
View File
@@ -170,6 +170,7 @@ fn sketch_json(s: &Session, id: u64) -> Option<Value> {
"profiles": profiles,
"links": sk.links,
"wires": sk.wires.iter().map(|w| json!({"id": w.id, "points": w.pts.len(), "start": w.pts.first(), "end": w.pts.last(), "link": w.link})).collect::<Vec<_>>(),
"visible": crate::view::sketch_shown(s, id),
}))
}
@@ -207,18 +208,27 @@ fn inspect(s: &mut Session, p: &Value) -> Result<Value> {
})
.collect();
let with_measure = bool_(p, "measure").unwrap_or(false);
let bodies: Vec<Value> =
st.bodies.iter().map(|b| if with_measure { measure_json(b) } else { json!({"name": b.name, "feature": b.feature}) }).collect();
let bodies: Vec<Value> = st
.bodies
.iter()
.map(|b| {
let mut v = if with_measure { measure_json(b) } else { json!({"name": b.name, "feature": b.feature}) };
if let Some(o) = v.as_object_mut() {
o.insert("visible".into(), json!(!s.visibility.hidden_bodies.contains(&b.name)));
}
v
})
.collect();
let sketches: Vec<Value> = st
.sketches
.iter()
.map(|ss| json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "curves": ss.sketch.curves.len(), "profiles": ss.profiles.len()}))
.map(|ss| json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "curves": ss.sketch.curves.len(), "profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)}))
.collect();
Ok(json!({
"name": s.doc.name, "units": s.doc.units, "path": s.path, "dirty": s.is_dirty(), "revision": s.revision,
"active_sketch": s.active_sketch, "marker": marker,
"params": params, "timeline": timeline, "bodies": bodies, "sketches": sketches,
"selection": s.selection, "undo": s.undo.len(), "redo": s.redo.len(),
"selection": s.selection, "undo": s.undo.len(), "redo": s.redo.len(), "hidden_origin": s.visibility.hidden_origin,
}))
}
+18
View File
@@ -168,6 +168,22 @@ pub struct Session {
pub section: Option<(Vec3, Vec3)>,
/// Surface analysis shading the model (zebra, draft, curvature map).
pub analysis: Option<SurfaceAnalysis>,
/// What is hidden or shown in the view (`browser.visibility`).
pub visibility: Visibility,
}
/// Items hidden or shown one by one (view state, not part of the design and not an undo step).
#[derive(Clone, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Visibility {
/// Bodies by name.
pub hidden_bodies: Vec<String>,
/// Origin items (O, X, Y, Z, XY, XZ, YZ) and construction geometry, by name.
pub hidden_origin: Vec<String>,
/// Sketches by feature id.
pub hidden_sketches: Vec<u64>,
/// Finished sketches shown although a feature uses them.
pub shown_sketches: Vec<u64>,
}
/// A way of shading the model's faces to judge their shape (view state, not part of the design).
@@ -219,6 +235,7 @@ impl Session {
clipboard: Vec::new(),
section: None,
analysis: None,
visibility: Visibility::default(),
}
}
@@ -360,6 +377,7 @@ impl Session {
clipboard: self.clipboard.clone(),
section: None,
analysis: None,
visibility: Visibility::default(),
}
}
+12 -5
View File
@@ -41,10 +41,11 @@ pub mod colors {
pub fn bounds(s: &Session) -> Aabb3 {
let st = s.world_state();
let mut b = Aabb3::EMPTY;
for body in &st.bodies {
let v = &s.visibility;
for body in st.bodies.iter().filter(|x| !v.hidden_bodies.contains(&x.name)) {
b = b.union(&body.mesh().bounds());
}
for ss in &st.sketches {
for ss in st.sketches.iter().filter(|x| !v.hidden_sketches.contains(&x.feature)) {
if let Some((lo, hi)) = ss.sketch.bounds() {
for p in [lo, hi] {
b.add(ss.plane.to_world(p));
@@ -151,7 +152,7 @@ pub fn scene(s: &Session, cam: &Camera) -> Scene {
for (d, col) in [(Vec3::X, colors::AXIS_X), (Vec3::Y, colors::AXIS_Y), (Vec3::Z, colors::AXIS_Z)] {
sc.lines.push(SceneLine { points: vec![Vec3::ZERO, d * axis], color: col, width: 1.6, on_top: false });
}
for body in &st.bodies {
for body in st.bodies.iter().filter(|b| !s.visibility.hidden_bodies.contains(&b.name)) {
let selected = s.selection.iter().any(|x| matches!(x, Sel::Body { name } if *name == body.name));
let mesh = body.mesh();
// The body's appearance (or imported colour) and faces with looks of their own, as in
@@ -174,8 +175,7 @@ pub fn scene(s: &Session, cam: &Camera) -> Scene {
}
for ss in &st.sketches {
let active = s.active_sketch == Some(ss.feature);
// Finished sketches are hidden once a feature uses them, like Fusion's default.
if !active && sketch_consumed(s, ss.feature) {
if !sketch_shown(s, ss.feature) {
continue;
}
for (pts, col, cons) in sketch_lines(&ss.sketch, &ss.plane, active, &ss.report.curve_determined) {
@@ -185,6 +185,13 @@ pub fn scene(s: &Session, cam: &Camera) -> Scene {
sc
}
/// Is a sketch drawn? The active one is; others unless hidden. Finished sketches are hidden
/// once a feature uses them, like Fusion's default, unless shown one by one.
pub fn sketch_shown(s: &Session, id: u64) -> bool {
let v = &s.visibility;
s.active_sketch == Some(id) || (!v.hidden_sketches.contains(&id) && (v.shown_sketches.contains(&id) || !sketch_consumed(s, id)))
}
/// Is a sketch used by a later feature?
pub fn sketch_consumed(s: &Session, id: u64) -> bool {
use solvecraft_doc::FeatureKind;
+17
View File
@@ -213,6 +213,23 @@ fn batch_stops_at_first_error_and_rolls_back() {
assert_eq!(tool(&mut s, "measure", json!({}))["body_count"], 2);
}
#[test]
fn helper_sketches_can_be_hidden() {
let mut s = server();
tool(&mut s, "execute", json!({"command": "solid.box", "params": {"length": 40, "width": 40, "height": 2}}));
let sk = tool(&mut s, "execute", json!({"command": "sketch.create", "params": {"plane": "XY"}}))["sketch"].clone();
tool(&mut s, "execute", json!({"command": "sketch.line", "params": {"points": [[0, 0], [40, 0]], "construction": true}}));
tool(&mut s, "execute", json!({"command": "sketch.finish"}));
let visible = |s: &mut Server| tool(s, "inspect_design", json!({"measure": false}))["sketches"][0]["visible"].clone();
assert_eq!(visible(&mut s), true);
tool(&mut s, "execute", json!({"command": "browser.visibility", "params": {"items": [sk], "visible": false}}));
assert_eq!(visible(&mut s), false);
let r = raw_tool(&mut s, "execute", json!({"command": "browser.visibility", "params": {"items": ["Nope"], "visible": false}}));
assert_eq!(r["result"]["isError"], true);
tool(&mut s, "execute", json!({"command": "browser.visibility", "params": {"folder": "sketches", "visible": true}}));
assert_eq!(visible(&mut s), true);
}
#[test]
fn unknown_tools_and_bad_arguments_are_clean_errors() {
let mut s = server();
+1 -1
View File
@@ -27,7 +27,7 @@ pub fn map(app: &SolveApp) -> Option<AccessMap> {
return Some(m);
}
let st = app.session.world_state();
let meshes: Vec<_> = st.bodies.iter().filter(|b| !app.ui.hidden_bodies.contains(&b.name)).map(|b| b.mesh()).collect();
let meshes: Vec<_> = st.bodies.iter().filter(|b| !app.session.visibility.hidden_bodies.contains(&b.name)).map(|b| b.mesh()).collect();
// Every new map gets a new version (the GPU uploads it once).
let version = VERSION.with(|v| {
v.set(v.get() + 1);
+24 -21
View File
@@ -80,19 +80,19 @@ impl OccMove {
/// Is a sketch drawn in the viewport?
pub fn sketch_visible(app: &SolveApp, id: u64) -> bool {
if app.ui.hidden_sketches.contains(&id) {
if app.session.visibility.hidden_sketches.contains(&id) {
return false;
}
app.ui.shown_sketches.contains(&id) || (app.ui.show_sketches && !solvecraft_engine::view::sketch_consumed(&app.session, id))
app.session.visibility.shown_sketches.contains(&id) || (app.ui.show_sketches && !solvecraft_engine::view::sketch_consumed(&app.session, id))
}
pub fn set_sketch_visible(app: &mut SolveApp, id: u64, on: bool) {
app.ui.hidden_sketches.retain(|x| *x != id);
app.ui.shown_sketches.retain(|x| *x != id);
app.session.visibility.hidden_sketches.retain(|x| *x != id);
app.session.visibility.shown_sketches.retain(|x| *x != id);
if on {
app.ui.shown_sketches.push(id);
app.session.visibility.shown_sketches.push(id);
} else {
app.ui.hidden_sketches.push(id);
app.session.visibility.hidden_sketches.push(id);
}
}
@@ -126,7 +126,7 @@ pub fn group_items(app: &SolveApp, id: u64) -> Vec<Item> {
];
if g.folder == "bodies" {
let bodies = group_bodies(app, id);
let hidden = !bodies.is_empty() && bodies.iter().all(|b| app.ui.hidden_bodies.contains(b));
let hidden = !bodies.is_empty() && bodies.iter().all(|b| app.session.visibility.hidden_bodies.contains(b));
v.push(Item::sep());
v.push(Item::action(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": bodies })));
v.push(Item::action("ui.isolate", "Isolate", "").with(json!({ "bodies": bodies })));
@@ -158,8 +158,8 @@ pub fn folder_items(app: &SolveApp, component: u64, folder: &str) -> Vec<Item> {
/// The menu of the Origin folder.
pub fn origin_items(app: &SolveApp) -> Vec<Item> {
let planes = ["XY", "XZ", "YZ"].iter().all(|p| !app.ui.hidden_origin.iter().any(|h| h == p));
let axes = ["X", "Y", "Z"].iter().all(|p| !app.ui.hidden_origin.iter().any(|h| h == p));
let planes = ["XY", "XZ", "YZ"].iter().all(|p| !app.session.visibility.hidden_origin.iter().any(|h| h == p));
let axes = ["X", "Y", "Z"].iter().all(|p| !app.session.visibility.hidden_origin.iter().any(|h| h == p));
vec![
Item::action("ui.origin", "Show/Hide", "eye").key("V"),
Item::action("ui.originAll", "Show All", "eye"),
@@ -520,7 +520,7 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize,
let open = is_open(app, &key, true);
let active = app.session.active_component == id;
let bodies = crate::context_menu::component_bodies(app, id);
let visible = bodies.is_empty() || bodies.iter().any(|b| !app.ui.hidden_bodies.contains(b));
let visible = bodies.is_empty() || bodies.iter().any(|b| !app.session.visibility.hidden_bodies.contains(b));
let grounded = app.session.doc.occurrence_of(id).is_some_and(|o| o.grounded);
let faded = app.session.active_component != 0 && !app.session.doc.component_within(id, app.session.active_component) && !active;
let name = component_name(app, id);
@@ -546,9 +546,12 @@ fn component_rows(app: &mut SolveApp, ui: &mut egui::Ui, id: u64, depth: usize,
}
if r.eye {
if visible {
app.ui.hidden_bodies.extend(bodies.iter().filter(|b| !app.ui.hidden_bodies.contains(b)).cloned().collect::<Vec<_>>());
app.session
.visibility
.hidden_bodies
.extend(bodies.iter().filter(|b| !app.session.visibility.hidden_bodies.contains(b)).cloned().collect::<Vec<_>>());
} else {
app.ui.hidden_bodies.retain(|b| !bodies.contains(b));
app.session.visibility.hidden_bodies.retain(|b| !bodies.contains(b));
}
}
if r.clicked {
@@ -661,7 +664,7 @@ fn origin_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
("YZ", "plane", "YZ"),
];
for (k, icon, label) in items {
let visible = !app.ui.hidden_origin.iter().any(|h| h == k);
let visible = !app.session.visibility.hidden_origin.iter().any(|h| h == k);
let sel = match icon {
"plane" => Some(Sel::Plane { name: k.into() }),
"axis" => Some(Sel::Axis { name: k.into() }),
@@ -675,9 +678,9 @@ fn origin_rows(app: &mut SolveApp, ui: &mut egui::Ui, depth: usize) {
);
if r.eye {
if visible {
app.ui.hidden_origin.push(k.into());
app.session.visibility.hidden_origin.push(k.into());
} else {
app.ui.hidden_origin.retain(|h| h != k);
app.session.visibility.hidden_origin.retain(|h| h != k);
}
}
if r.clicked
@@ -1001,7 +1004,7 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec<Entry> {
row_key: format!("b:{}", b.name),
label: b.name.clone(),
icon: "body",
visible: !app.ui.hidden_bodies.contains(&b.name),
visible: !app.session.visibility.hidden_bodies.contains(&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),
@@ -1040,7 +1043,7 @@ fn entries(app: &SolveApp, comp: u64, folder: &str) -> Vec<Entry> {
row_key: format!("p:{}", f.id),
label: f.name.clone(),
icon: "plane",
visible: !app.ui.hidden_origin.contains(&f.name),
visible: !app.session.visibility.hidden_origin.contains(&f.name),
selected: sel.iter().any(|x| matches!(x, Sel::Plane { name } if *name == f.name)),
color: None,
locked: false,
@@ -1079,9 +1082,9 @@ fn set_visible(app: &mut SolveApp, folder: &str, items: &[&Entry], on: bool) {
for e in items {
match folder {
"bodies" => {
app.ui.hidden_bodies.retain(|b| *b != e.key);
app.session.visibility.hidden_bodies.retain(|b| *b != e.key);
if !on {
app.ui.hidden_bodies.push(e.key.clone());
app.session.visibility.hidden_bodies.push(e.key.clone());
}
}
"sketches" => {
@@ -1090,9 +1093,9 @@ fn set_visible(app: &mut SolveApp, folder: &str, items: &[&Entry], on: bool) {
}
}
_ => {
app.ui.hidden_origin.retain(|p| *p != e.label);
app.session.visibility.hidden_origin.retain(|p| *p != e.label);
if !on {
app.ui.hidden_origin.push(e.label.clone());
app.session.visibility.hidden_origin.push(e.label.clone());
}
}
}
+23 -22
View File
@@ -358,7 +358,7 @@ fn viewport_items(app: &SolveApp) -> Vec<Item> {
if has(|s| matches!(s, Sel::Face { .. })) {
let face = sel.iter().find_map(|s| if let Sel::Face { body, .. } = s { Some(body.clone()) } else { None }).unwrap_or_default();
let (feature, sketch) = body_feature_and_sketch(app, &face);
let hidden = app.ui.hidden_bodies.contains(&face);
let hidden = app.session.visibility.hidden_bodies.contains(&face);
v.push(cmd(app, "sketch.create", "Create Sketch"));
v.push(cmd(app, "solid.extrude", "Extrude"));
v.push(cmd(app, "construct.plane.offset", "Offset Plane"));
@@ -379,7 +379,7 @@ fn viewport_items(app: &SolveApp) -> Vec<Item> {
v.push(cmd(app, "inspect.measure", "Measure"));
} else if has(|s| matches!(s, Sel::Edge { .. })) {
let body = sel.iter().find_map(|s| if let Sel::Edge { body, .. } = s { Some(body.clone()) } else { None }).unwrap_or_default();
let hidden = app.ui.hidden_bodies.contains(&body);
let hidden = app.session.visibility.hidden_bodies.contains(&body);
v.push(cmd(app, "solid.fillet", "Fillet"));
v.push(cmd(app, "solid.chamfer", "Chamfer"));
v.push(Item::sep());
@@ -403,9 +403,9 @@ fn viewport_items(app: &SolveApp) -> Vec<Item> {
v.push(act("ui.fit", "Fit", "fit"));
v.push(act("ui.home", "Home View", "home"));
v.push(Item::sep());
let anything_hidden = !app.ui.hidden_bodies.is_empty() || !app.ui.hidden_sketches.is_empty();
let anything_hidden = !app.session.visibility.hidden_bodies.is_empty() || !app.session.visibility.hidden_sketches.is_empty();
v.push(act("ui.showAll", "Show All", "eye").on(anything_hidden));
v.push(act("ui.showAll", "Unisolate", "").on(!app.ui.hidden_bodies.is_empty()));
v.push(act("ui.showAll", "Unisolate", "").on(!app.session.visibility.hidden_bodies.is_empty()));
v.push(Item::sep());
v.push(cmd(app, "solid.extrude", "Extrude"));
v.push(cmd(app, "solid.fillet", "Fillet"));
@@ -428,7 +428,7 @@ fn viewport_items(app: &SolveApp) -> Vec<Item> {
fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec<Item> {
let names = json!(bodies);
let all_hidden = bodies.iter().all(|b| app.ui.hidden_bodies.contains(b));
let all_hidden = bodies.iter().all(|b| app.session.visibility.hidden_bodies.contains(b));
let one = bodies.len() == 1;
let locked = bodies.iter().all(|b| app.ui.locked_bodies.contains(b));
let items = json!(bodies.iter().map(|b| json!({"type": "body", "name": b})).collect::<Vec<_>>());
@@ -506,7 +506,7 @@ fn component_items(app: &SolveApp, id: u64) -> Vec<Item> {
let occ = doc.occurrence_of(id);
let active = app.session.active_component == id;
let bodies = component_bodies(app, id);
let hidden = !bodies.is_empty() && bodies.iter().all(|b| app.ui.hidden_bodies.contains(b));
let hidden = !bodies.is_empty() && bodies.iter().all(|b| app.session.visibility.hidden_bodies.contains(b));
let names = json!(bodies);
let mut v = Vec::new();
if id != 0 {
@@ -666,18 +666,19 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
"ui.finishSketch" => app.finish_sketch(),
"ui.hide" => {
for b in bodies {
if !app.ui.hidden_bodies.contains(&b) {
app.ui.hidden_bodies.push(b);
if !app.session.visibility.hidden_bodies.contains(&b) {
app.session.visibility.hidden_bodies.push(b);
}
}
}
"ui.show" => app.ui.hidden_bodies.retain(|b| !bodies.contains(b)),
"ui.show" => app.session.visibility.hidden_bodies.retain(|b| !bodies.contains(b)),
"ui.showAll" => {
app.ui.hidden_bodies.clear();
app.ui.hidden_sketches.clear();
app.session.visibility.hidden_bodies.clear();
app.session.visibility.hidden_sketches.clear();
}
"ui.isolate" => {
app.ui.hidden_bodies = app.session.world_state().bodies.iter().map(|b| b.name.clone()).filter(|b| !bodies.contains(b)).collect();
app.session.visibility.hidden_bodies =
app.session.world_state().bodies.iter().map(|b| b.name.clone()).filter(|b| !bodies.contains(b)).collect();
}
"ui.lock" => {
let locked = bodies.iter().all(|b| app.ui.locked_bodies.contains(b));
@@ -822,14 +823,14 @@ pub fn run_item(app: &mut SolveApp, item: &Item, at: Pos2) {
}
"ui.originAll" => {
app.ui.show_origin = true;
app.ui.hidden_origin.retain(|h| !["O", "X", "Y", "Z", "XY", "XZ", "YZ"].contains(&h.as_str()));
app.session.visibility.hidden_origin.retain(|h| !["O", "X", "Y", "Z", "XY", "XZ", "YZ"].contains(&h.as_str()));
}
"ui.originPart" => {
let show = p.get("show").and_then(Value::as_bool).unwrap_or(true);
for k in strs(&p, "keys") {
app.ui.hidden_origin.retain(|h| *h != k);
app.session.visibility.hidden_origin.retain(|h| *h != k);
if !show {
app.ui.hidden_origin.push(k);
app.session.visibility.hidden_origin.push(k);
}
}
}
@@ -925,16 +926,16 @@ pub fn toggle_visibility(app: &mut SolveApp) {
})
.collect();
let canvases = app.tree.picked_canvases.clone();
let any_visible = bodies.iter().any(|b| !app.ui.hidden_bodies.contains(b))
let any_visible = bodies.iter().any(|b| !app.session.visibility.hidden_bodies.contains(b))
|| sketches.iter().any(|s| crate::browser::sketch_visible(app, *s))
|| origin.iter().any(|o| !app.ui.hidden_origin.contains(o))
|| origin.iter().any(|o| !app.session.visibility.hidden_origin.contains(o))
|| app.session.doc.canvases.iter().any(|c| canvases.contains(&c.id) && c.visible);
let hide = any_visible;
app.ui.hidden_bodies.retain(|b| !bodies.contains(b));
app.ui.hidden_origin.retain(|o| !origin.contains(o));
app.session.visibility.hidden_bodies.retain(|b| !bodies.contains(b));
app.session.visibility.hidden_origin.retain(|o| !origin.contains(o));
if hide {
app.ui.hidden_bodies.extend(bodies);
app.ui.hidden_origin.extend(origin);
app.session.visibility.hidden_bodies.extend(bodies);
app.session.visibility.hidden_origin.extend(origin);
}
for s in sketches {
crate::browser::set_sketch_visible(app, s, !hide);
@@ -1007,7 +1008,7 @@ fn commit_rename(app: &mut SolveApp, r: &Rename) {
match &r.what {
RenameWhat::Body(old) => {
if app.run("body.rename", json!({ "body": old, "name": name })).is_ok() {
for list in [&mut app.ui.hidden_bodies, &mut app.ui.locked_bodies] {
for list in [&mut app.session.visibility.hidden_bodies, &mut app.ui.locked_bodies] {
for b in list.iter_mut().filter(|b| *b == old) {
*b = name.clone();
}
+18 -5
View File
@@ -119,6 +119,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
"document.inspect" => wrap(app.session.execute("document.inspect", p).map_err(|e| e.to_string())),
"ui.inspect" => ok(json!({
"ui": serde_json::to_value(&app.ui).unwrap_or_default(),
"visibility": app.session.visibility,
"viewport": app.viewport.rect.map(|r| json!([r.left(), r.top(), r.width(), r.height()])),
"camera": app.cam,
"tool": app.tool.as_ref().map(|t| t.cmd),
@@ -149,18 +150,30 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
ok(json!({"open": app.home.open, "recent": app.home.recent}))
}
"ui.set" => {
// What is hidden lives in the session (`browser.visibility`); `ui.set` still sets it.
const VIS: [(&str, &str); 4] = [
("hiddenBodies", "hidden_bodies"),
("hiddenOrigin", "hidden_origin"),
("hiddenSketches", "hidden_sketches"),
("shownSketches", "shown_sketches"),
];
let mut cur = serde_json::to_value(&app.ui).unwrap_or(json!({}));
if let (Some(o), Some(src)) = (cur.as_object_mut(), p.as_object()) {
let mut vis = serde_json::to_value(&app.session.visibility).unwrap_or(json!({}));
if let (Some(o), Some(vo), Some(src)) = (cur.as_object_mut(), vis.as_object_mut(), p.as_object()) {
for (k, v) in src {
o.insert(k.clone(), v.clone());
match VIS.iter().find(|(camel, _)| camel == k) {
Some((_, snake)) => vo.insert(snake.to_string(), v.clone()),
None => o.insert(k.clone(), v.clone()),
};
}
}
match serde_json::from_value::<crate::UiState>(cur) {
Ok(u) => {
match (serde_json::from_value::<crate::UiState>(cur), serde_json::from_value::<solvecraft_engine::Visibility>(vis)) {
(Ok(u), Ok(v)) => {
app.ui = u;
app.session.visibility = v;
ok(serde_json::to_value(&app.ui).unwrap_or_default())
}
Err(e) => err(e),
(Err(e), _) | (_, Err(e)) => err(e),
}
}
"ui.view" => {
-12
View File
@@ -27,10 +27,6 @@ pub struct DocSlot {
/// The per-design part of the UI state (what is hidden, shown or locked).
#[derive(Clone, Debug, Default)]
struct DocView {
hidden_bodies: Vec<String>,
hidden_origin: Vec<String>,
hidden_sketches: Vec<u64>,
shown_sketches: Vec<u64>,
hidden_profiles: Vec<u64>,
shown_dims: Vec<u64>,
locked_bodies: Vec<String>,
@@ -63,10 +59,6 @@ impl Documents {
fn take_view(app: &mut SolveApp) -> DocView {
let u = &mut app.ui;
DocView {
hidden_bodies: std::mem::take(&mut u.hidden_bodies),
hidden_origin: std::mem::take(&mut u.hidden_origin),
hidden_sketches: std::mem::take(&mut u.hidden_sketches),
shown_sketches: std::mem::take(&mut u.shown_sketches),
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),
@@ -75,10 +67,6 @@ fn take_view(app: &mut SolveApp) -> DocView {
fn put_view(app: &mut SolveApp, v: DocView) {
let u = &mut app.ui;
u.hidden_bodies = v.hidden_bodies;
u.hidden_origin = v.hidden_origin;
u.hidden_sketches = v.hidden_sketches;
u.shown_sketches = v.shown_sketches;
u.hidden_profiles = v.hidden_profiles;
u.shown_dims = v.shown_dims;
u.locked_bodies = v.locked_bodies;
+1 -11
View File
@@ -101,9 +101,6 @@ pub struct UiState {
pub show_grid: bool,
pub show_origin: bool,
pub perspective: bool,
pub hidden_bodies: Vec<String>,
/// Origin items (O, X, Y, Z, XY, XZ, YZ) and construction planes hidden one by one.
pub hidden_origin: Vec<String>,
pub show_sketches: bool,
pub palette_open: bool,
/// Dark theme (the default); a preference kept between runs.
@@ -118,9 +115,6 @@ pub struct UiState {
pub ground_shadow: bool,
/// The world's up axis (Z or Y; #3): the camera, view cube, home view and ground follow it.
pub up_axis: solvecraft_engine::render::UpAxis,
/// Sketches hidden one by one, and finished sketches shown although a feature uses them.
pub hidden_sketches: Vec<u64>,
pub shown_sketches: Vec<u64>,
/// Sketches whose closed profiles are not shaded.
pub hidden_profiles: Vec<u64>,
/// Finished sketches whose dimensions are shown.
@@ -140,8 +134,6 @@ impl Default for UiState {
show_grid: true,
show_origin: true,
perspective: false,
hidden_bodies: Vec::new(),
hidden_origin: Vec::new(),
show_sketches: true,
palette_open: false,
dark: true,
@@ -150,8 +142,6 @@ impl Default for UiState {
visual_style: 0,
ground_shadow: true,
up_axis: Default::default(),
hidden_sketches: Vec::new(),
shown_sketches: Vec::new(),
hidden_profiles: Vec::new(),
shown_dims: Vec::new(),
locked_bodies: Vec::new(),
@@ -594,7 +584,7 @@ impl SolveApp {
pub fn origin_visible(&self) -> bool {
self.ui.show_origin
|| (self.dialog.as_ref().is_some_and(|d| matches!(d.kind, dialogs::Kind::Sketch))
&& !self.session.world_state().bodies.iter().any(|b| !self.ui.hidden_bodies.contains(&b.name)))
&& !self.session.world_state().bodies.iter().any(|b| !self.session.visibility.hidden_bodies.contains(&b.name)))
}
pub fn set_status(&mut self, s: impl Into<String>, error: bool) {
+8 -8
View File
@@ -58,7 +58,7 @@ fn face_menu_hides_and_finds_the_body() {
context_menu::run_item(&mut app, &find(&items, "Find in Timeline"), pos2(0.0, 0.0));
assert!(matches!(app.session.selection.as_slice(), [solvecraft_engine::Sel::Feature { .. }]));
context_menu::run_item(&mut app, &find(&items, "Show/Hide"), pos2(0.0, 0.0));
assert_eq!(app.ui.hidden_bodies, vec![b]);
assert_eq!(app.session.visibility.hidden_bodies, vec![b]);
}
#[test]
@@ -69,8 +69,8 @@ fn body_menu_isolates_renames_locks_and_deletes() {
let t = Target::Body { name: b.clone() };
let items = context_menu::items(&app, &t);
context_menu::run_item(&mut app, &find(&items, "Isolate"), pos2(0.0, 0.0));
assert_eq!(app.ui.hidden_bodies.len(), 1, "the other body is hidden");
assert!(!app.ui.hidden_bodies.contains(&b));
assert_eq!(app.session.visibility.hidden_bodies.len(), 1, "the other body is hidden");
assert!(!app.session.visibility.hidden_bodies.contains(&b));
// Rename through the rename box.
context_menu::run_item(&mut app, &find(&items, "Rename"), pos2(10.0, 10.0));
assert!(context_menu::finish_rename(&mut app, Some("Base"), true));
@@ -184,7 +184,7 @@ fn folder_and_group_menus_make_rename_and_drop_groups() {
assert!(context_menu::finish_rename(&mut app, Some("Parts"), true));
assert_eq!(app.session.doc.browser_groups[0].name, "Parts");
act(&mut app, &t, "Show/Hide");
assert_eq!(app.ui.hidden_bodies.len(), 2);
assert_eq!(app.session.visibility.hidden_bodies.len(), 2);
act(&mut app, &t, "Ungroup");
assert!(app.session.doc.browser_groups.is_empty());
act(&mut app, &folder, "New Group");
@@ -351,7 +351,7 @@ fn menus_follow_fusion_order() {
let origin = context_menu::items(&app, &Target::Origin);
order(&origin, &["Show/Hide", "Show All", "Hide Planes", "Hide Axes"]);
context_menu::run_item(&mut app, &find(&origin, "Hide Planes"), pos2(0.0, 0.0));
assert!(["XY", "XZ", "YZ"].iter().all(|p| app.ui.hidden_origin.iter().any(|h| h == p)));
assert!(["XY", "XZ", "YZ"].iter().all(|p| app.session.visibility.hidden_origin.iter().any(|h| h == p)));
app.run("select.clear", json!({})).unwrap();
let empty = context_menu::items(&app, &Target::Viewport);
order(&empty, &["Pan", "Zoom", "Orbit", "Show All", "Unisolate", "Extrude", "Fillet"]);
@@ -367,17 +367,17 @@ fn v_toggles_what_is_selected() {
app.run("select.set", json!({"items": [{"type": "body", "name": b}, {"type": "plane", "name": "XY"}, {"type": "feature", "id": sk}]})).unwrap();
crate::browser::set_sketch_visible(&mut app, sk, true);
context_menu::toggle_visibility(&mut app);
assert!(app.ui.hidden_bodies.contains(&b) && app.ui.hidden_origin.iter().any(|h| h == "XY"));
assert!(app.session.visibility.hidden_bodies.contains(&b) && app.session.visibility.hidden_origin.iter().any(|h| h == "XY"));
assert!(!crate::browser::sketch_visible(&app, sk));
context_menu::toggle_visibility(&mut app);
assert!(app.ui.hidden_bodies.is_empty() && app.ui.hidden_origin.is_empty());
assert!(app.session.visibility.hidden_bodies.is_empty() && app.session.visibility.hidden_origin.is_empty());
assert!(crate::browser::sketch_visible(&app, sk));
// Components picked in the browser hide their bodies.
app.run("component.from_bodies", json!({"bodies": [b.clone()]})).unwrap();
app.run("select.clear", json!({})).unwrap();
app.tree.picked_components = vec![app.session.doc.components[0].id];
context_menu::toggle_visibility(&mut app);
assert_eq!(app.ui.hidden_bodies, vec![b]);
assert_eq!(app.session.visibility.hidden_bodies, vec![b]);
assert_eq!(find(&context_menu::items(&app, &Target::Body { name: body(&app) }), "Show/Hide").shortcut, "V");
}
+1 -2
View File
@@ -730,8 +730,7 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
}
"hidden" => {
// Bodies hidden in the view.
let h = &ui["ui"]["hidden_bodies"];
let h = if h.is_null() { &ui["ui"]["hiddenBodies"] } else { h };
let h = &ui["visibility"]["hidden_bodies"];
if h != v {
return Err(format!("hidden bodies: got {h}, want {v}"));
}
+27 -23
View File
@@ -192,8 +192,9 @@ fn visible_sketches(app: &SolveApp) -> Vec<u64> {
.iter()
.filter(|s| {
app.session.active_sketch == Some(s.feature)
|| (!app.ui.hidden_sketches.contains(&s.feature)
&& (app.ui.shown_sketches.contains(&s.feature) || (app.ui.show_sketches && !sketch_consumed(&app.session, s.feature))))
|| (!app.session.visibility.hidden_sketches.contains(&s.feature)
&& (app.session.visibility.shown_sketches.contains(&s.feature)
|| (app.ui.show_sketches && !sketch_consumed(&app.session, s.feature))))
})
.map(|s| s.feature)
.collect()
@@ -210,9 +211,9 @@ fn scene_key(app: &SolveApp) -> u64 {
app.cam.up.hash(&mut h);
(app.cam.pitch > 0.0).hash(&mut h);
app.ui.show_sketches.hash(&mut h);
app.ui.hidden_bodies.hash(&mut h);
app.ui.hidden_sketches.hash(&mut h);
app.ui.shown_sketches.hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.session.visibility.hidden_sketches.hash(&mut h);
app.session.visibility.shown_sketches.hash(&mut h);
app.ui.hidden_profiles.hash(&mut h);
app.session.active_component.hash(&mut h);
app.preview.replaced.hash(&mut h);
@@ -262,7 +263,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
}
}
for b in &st.bodies {
if app.ui.hidden_bodies.contains(&b.name) || app.preview.replaced.contains(&b.name) {
if app.session.visibility.hidden_bodies.contains(&b.name) || app.preview.replaced.contains(&b.name) {
continue;
}
// With a component active, bodies outside it are drawn faded.
@@ -326,7 +327,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
// Section: the outline of each body where the plane cuts it.
if let Some((o, n)) = section_plane(app) {
let col = c4(tk.text.gamma_multiply(0.9));
for b in st.bodies.iter().filter(|b| !app.ui.hidden_bodies.contains(&b.name)) {
for b in st.bodies.iter().filter(|b| !app.session.visibility.hidden_bodies.contains(&b.name)) {
for (a, c) in section_segments(&b.mesh(), o, n) {
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
}
@@ -367,7 +368,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solvecraft_engine::doc::ModelState) {
// The bodies' box in the camera's Z-up frame (the ground is its XY plane).
let mut b = solvecraft_engine::geom::Aabb3::EMPTY;
for x in st.bodies.iter().filter(|x| !app.ui.hidden_bodies.contains(&x.name)) {
for x in st.bodies.iter().filter(|x| !app.session.visibility.hidden_bodies.contains(&x.name)) {
let bb = x.mesh().bounds();
if bb.is_empty() {
continue;
@@ -484,7 +485,7 @@ pub fn origin_planes(app: &SolveApp) -> Vec<(&'static str, Vec3, [Vec3; 4])> {
let creating = !app.ui.show_origin && app.dialog.as_ref().is_some_and(|d| matches!(d.kind, crate::dialogs::Kind::Sketch));
origin_quads(origin_size(app.cam.half_height()))
.into_iter()
.filter(|(n, _, _)| creating || !app.ui.hidden_origin.iter().any(|h| h == n))
.filter(|(n, _, _)| creating || !app.session.visibility.hidden_origin.iter().any(|h| h == n))
.collect()
}
@@ -492,7 +493,10 @@ pub fn origin_axes(app: &SolveApp) -> Vec<(&'static str, Vec3)> {
if !app.origin_visible() {
return Vec::new();
}
[("X", Vec3::X), ("Y", Vec3::Y), ("Z", Vec3::Z)].into_iter().filter(|(n, _)| !app.ui.hidden_origin.iter().any(|h| h == n)).collect()
[("X", Vec3::X), ("Y", Vec3::Y), ("Z", Vec3::Z)]
.into_iter()
.filter(|(n, _)| !app.session.visibility.hidden_origin.iter().any(|h| h == n))
.collect()
}
/// Construction axes shown, as segments: across the model where the axis passes it (a little
@@ -503,7 +507,7 @@ pub fn construction_axis_segments(app: &SolveApp) -> Vec<(String, Vec3, Vec3)> {
let bb = solvecraft_engine::view::bounds(&app.session);
solvecraft_engine::view::construction_axes(&app.session)
.into_iter()
.filter(|(_, n, _, _)| !app.ui.hidden_origin.contains(n))
.filter(|(_, n, _, _)| !app.session.visibility.hidden_origin.contains(n))
.map(|(_, name, o, d)| {
let (mut lo, mut hi) = (-h, h);
if !bb.is_empty() {
@@ -527,7 +531,7 @@ pub fn construction_axis_segments(app: &SolveApp) -> Vec<(String, Vec3, Vec3)> {
pub fn construction_points(app: &SolveApp) -> Vec<(String, Vec3)> {
solvecraft_engine::view::construction_points(&app.session)
.into_iter()
.filter(|(_, n, _)| !app.ui.hidden_origin.contains(n))
.filter(|(_, n, _)| !app.session.visibility.hidden_origin.contains(n))
.map(|(_, n, p)| (n, p))
.collect()
}
@@ -537,7 +541,7 @@ pub fn construction_quads(app: &SolveApp) -> Vec<(String, Vec3, [Vec3; 4])> {
let h = origin_size(app.cam.half_height()) * 1.2;
solvecraft_engine::view::construction_planes(&app.session)
.into_iter()
.filter(|(_, n, _)| !app.ui.hidden_origin.contains(n))
.filter(|(_, n, _)| !app.session.visibility.hidden_origin.contains(n))
.map(|(_, name, pl)| {
let (o, x, y) = (pl.origin, pl.x * h, pl.y * h);
(name, pl.normal(), [o - x - y, o + x - y, o + x + y, o - x + y])
@@ -625,7 +629,7 @@ fn pick_cached(app: &mut SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
(std::sync::Arc::as_ptr(&app.session.world_state()) as usize).hash(&mut h);
app.session.active_sketch.hash(&mut h);
app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).hash(&mut h);
app.ui.hidden_bodies.hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.ui.perspective.hash(&mut h);
(proj.rect.width().to_bits(), proj.rect.height().to_bits()).hash(&mut h);
let key = h.finish();
@@ -701,7 +705,7 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
// Nearest face along the ray (hides what is behind it).
let mut bestf: Option<(f64, Hit)> = None;
for b in &st.bodies {
if app.ui.hidden_bodies.contains(&b.name) {
if app.session.visibility.hidden_bodies.contains(&b.name) {
continue;
}
// Bodies whose box the ray misses (or enters behind the best hit so far) are skipped.
@@ -727,7 +731,7 @@ pub fn pick(app: &SolveApp, proj: &Proj, pos: Pos2) -> Vec<Hit> {
let mut bestv: Option<(f32, Hit)> = None;
let mut beste: Option<(f32, Hit)> = None;
for b in &st.bodies {
if app.ui.hidden_bodies.contains(&b.name) {
if app.session.visibility.hidden_bodies.contains(&b.name) {
continue;
}
// Only bodies whose outline on screen is near the cursor can have an edge under it.
@@ -1075,8 +1079,8 @@ fn highlight_key(app: &SolveApp) -> u64 {
crate::theme::is_dark().hash(&mut h);
app.preview.replaced.hash(&mut h);
app.origin_visible().hash(&mut h);
app.ui.hidden_origin.hash(&mut h);
app.ui.hidden_bodies.hash(&mut h);
app.session.visibility.hidden_origin.hash(&mut h);
app.session.visibility.hidden_bodies.hash(&mut h);
app.cam.half_height().to_bits().hash(&mut h);
app.cam.back().x.to_bits().hash(&mut h);
h.finish()
@@ -1159,7 +1163,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
}
Sel::Body { name } => {
if let Some(b) = st.body(name)
&& !app.ui.hidden_bodies.contains(name)
&& !app.session.visibility.hidden_bodies.contains(name)
{
let m = b.mesh();
for f in 0..b.body.face_count() {
@@ -1205,7 +1209,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
let k = t.hover_face_lift;
let lit =
|b: &solvecraft_engine::doc::ModelBody| Some(b.feature) == app.viewport.hover_feature || app.viewport.hover_bodies.contains(&b.name);
for b in st.bodies.iter().filter(|b| lit(b) && !app.ui.hidden_bodies.contains(&b.name)) {
for b in st.bodies.iter().filter(|b| lit(b) && !app.session.visibility.hidden_bodies.contains(&b.name)) {
let c = body_rgb(app, &b.name);
let m = b.mesh();
for f in 0..b.body.face_count() {
@@ -1783,7 +1787,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
} else {
let accept = app.dialog.as_ref().and_then(|d| d.active_input()).map(|i| i.accept).unwrap_or(BODIES);
for b in &st.bodies {
if app.ui.hidden_bodies.contains(&b.name) {
if app.session.visibility.hidden_bodies.contains(&b.name) {
continue;
}
let m = b.mesh();
@@ -1845,7 +1849,7 @@ fn box_select(app: &mut SolveApp, proj: &Proj, bx: BoxSel, add: bool) {
fn points_2d(app: &SolveApp, painter: &egui::Painter, proj: &Proj) {
let t = Tokens::get();
if app.origin_visible()
&& !app.ui.hidden_origin.iter().any(|h| h == "O")
&& !app.session.visibility.hidden_origin.iter().any(|h| h == "O")
&& let Some(o) = proj.to_screen(Vec3::ZERO)
{
painter.circle(o, 4.5, t.origin_point, Stroke::new(1.0, Color32::from_gray(110)));
@@ -2456,7 +2460,7 @@ mod tests {
// It is an axis for dialogs (pattern directions, revolve axes), and hiding it hides it.
let (o, dir) = crate::dialogs::axis_of(&app, &Sel::Axis { name: name.clone() }).unwrap();
assert!(o.dist(Vec3::new(0.0, 0.0, 20.0)) < 1e-6 && dir.dist(d) < 1e-6);
app.ui.hidden_origin.push(name);
app.session.visibility.hidden_origin.push(name);
assert!(construction_axis_segments(&app).is_empty());
}
}
+2 -2
View File
@@ -21,8 +21,8 @@ Commands never open dialogs when run this way. `solvecraft-cli commands` prints
| Method | Params | Notes |
|---|---|---|
| `ui.inspect` | — | UI state, viewport rect, camera, active tool/dialog, hover, renderer, timings, live preview (`active`, `busy`, `error`, `ms`, bodies it stands in for) |
| `ui.set` | any `UiState` field (`tab`, `perspective`, `showGrid`, `hiddenBodies`, `dark`, …) | `dark: false` switches to the light theme |
| `ui.inspect` | — | UI state, what is hidden (`visibility`: bodies, origin and construction items, sketches; set it with the `browser.visibility` command), viewport rect, camera, active tool/dialog, hover, renderer, timings, live preview (`active`, `busy`, `error`, `ms`, bodies it stands in for) |
| `ui.set` | any `UiState` field (`tab`, `perspective`, `showGrid`, `dark`, …), and `hiddenBodies`, `hiddenOrigin`, `hiddenSketches`, `shownSketches` (whole lists; `browser.visibility` changes single items) | `dark: false` switches to the light theme |
| `ui.view` | `view`: `front`, `back`, `top`, `bottom`, `left`, `right`, `iso`, `home`, `fit`; `animate` (bool, default false: snap) | |
| `ui.start` | `command` | like a toolbar click: starts the sketch tool or dialog for the command |
| `ui.click` | `x`, `y` (screen points), `button?`: left/right/middle, `shift?`, `ctrl?`, `double?` | real pointer input |
+1 -1
View File
@@ -51,7 +51,7 @@ Claude Desktop (`claude_desktop_config.json`), bridged to a running app:
| `list_commands` | `filter?` | The command catalog: id, label, tab/panel, shortcut, parameter docs, enabled now. |
| `execute` | `command`, `params?` | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. |
| `batch` | `commands: [{command, params}]`, `rollback?` (default true) | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`) and every result before it. |
| `inspect_design` | `measure?` (default true), `sketches?` (default true) | Parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts), sketches (curves, points, constraints, profiles, DOF, solve status), selection, undo depth. |
| `inspect_design` | `measure?` (default true), `sketches?` (default true) | Parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible), sketches (curves, points, constraints, profiles, DOF, solve status, visible), selection, undo depth. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`). |
| `measure` | `body?` | Volume (mm³), area (mm²), centre of mass, bbox and topology counts per body, with totals. |
| `body_topology` | `body` | Edges (index, midpoint, length, ends) and faces (index, area, centroid, normal). An edge midpoint is the `[x,y,z]` that picks that edge for fillet and chamfer. |
| `set_parameter` | `name`, `value` (number or expression), `unit?`, `comment?` | Create or change a user parameter and recompute; reports features that now fail. |