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:
1 parent
0321778d38
commit
81be4e74b8
16 files changed
+306
-118
No files matched your search
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user