diff --git a/crates/engine/src/inspect.rs b/crates/engine/src/inspect.rs index 1d1c7e0c7..b5d4bf329 100644 --- a/crates/engine/src/inspect.rs +++ b/crates/engine/src/inspect.rs @@ -81,6 +81,8 @@ fn layer_sel(l: &Layer, selected: &[photocraft_doc::LayerId]) -> Value { "blend": l.blend.label(), "clipped": l.clipped, "hasMask": l.mask.is_some(), + "hasVectorMask": l.vector_mask.is_some(), + "vectorMaskEnabled": l.vector_mask.as_ref().map(|mask| mask.enabled), "bounds": bounds, "selected": selected.contains(&l.id), "linkGroup": l.link_group, @@ -154,3 +156,56 @@ fn layer_sel(l: &Layer, selected: &[photocraft_doc::LayerId]) -> Value { } v } + +#[cfg(test)] +mod tests { + use super::*; + use photocraft_color::PixelFormat; + use photocraft_doc::{LayerMask, Path, VectorMask}; + + #[test] + fn document_inspect_reports_raster_and_vector_masks_when_nested() { + let cases = [(false, None), (true, None), (false, Some(true)), (false, Some(false)), (true, Some(true))]; + let mut session = Session::new(); + session.execute("file.new", json!({"width": 1, "height": 1})).unwrap(); + let layers = cases + .iter() + .map(|&(raster, enabled)| { + let mut layer = Layer::raster("Mask case", PixelFormat::RGBA8); + layer.mask = raster.then(LayerMask::reveal_all); + layer.vector_mask = enabled.map(|enabled| VectorMask { enabled, ..VectorMask::new(Path::default()) }); + layer + }) + .collect::>(); + let mut group = Layer::group("Inner", layers.clone()); + group.vector_mask = Some(VectorMask { enabled: false, ..VectorMask::new(Path::default()) }); + let mut roots = layers; + roots.push(Layer::group("Outer", vec![group])); + session + .edit("Mask fixtures", |doc, _| { + doc.layers = roots; + Ok(()) + }) + .unwrap(); + + let inspected = session.execute("document.inspect", json!({})).unwrap(); + + let roots = inspected["layers"].as_array().unwrap(); + let outer = &roots[0]; + let inner = &outer["children"][0]; + assert_eq!(outer.get("hasMask"), Some(&json!(false))); + assert_eq!(outer.get("hasVectorMask"), Some(&json!(false))); + assert_eq!(outer.get("vectorMaskEnabled"), Some(&Value::Null)); + assert_eq!(inner.get("hasMask"), Some(&json!(false))); + assert_eq!(inner.get("hasVectorMask"), Some(&json!(true))); + assert_eq!(inner.get("vectorMaskEnabled"), Some(&json!(false))); + for nodes in [&roots[1..], inner["children"].as_array().unwrap().as_slice()] { + assert_eq!(nodes.len(), cases.len()); + for (node, &(raster, enabled)) in nodes.iter().zip(cases.iter().rev()) { + assert_eq!(node.get("hasMask"), Some(&json!(raster))); + assert_eq!(node.get("hasVectorMask"), Some(&json!(enabled.is_some()))); + assert_eq!(node.get("vectorMaskEnabled"), Some(&json!(enabled))); + } + } + } +} diff --git a/docs/mcp.md b/docs/mcp.md index b3e46f67c..7ae522472 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -18,6 +18,9 @@ described in [development.md](development.md). UI tools require a running deskto - `command_batch`: ordered `steps`, optional `stop_on_error`; one history step per command, not an atomic transaction. Existing per-step results and reply budgets are preserved. - `doc_inspect`: layer tree, history and selection; an empty session returns `document:null`. + Every layer (including groups and nested children) reports raster-only `hasMask`, + `hasVectorMask` for vector mask presence, and `vectorMaskEnabled` (`true`/`false` when + present, `null` when absent). A disabled vector mask still has `hasVectorMask:true`. - `render_preview`: bounded PNG content; `index` and `max_side` (default 1024, maximum 2048). - `ui_inspect` and `ui_screenshot`: live app state/window in bridge mode.