feat: add boolean shape ops, visual undo tree, and Augmented Canvas branding
- Boolean operations (union, intersection, difference, xor) via i_overlay with Path shape rendering using earcutr triangulation - Visual undo tree with branching history, checkout, and fork (Cmd+H) using Arc-based snapshots for structural sharing - Consistent Augmented Canvas branding across app title, MCP server, CLI help text, and error messages - macOS .app bundle script and Info.plist for Finder integration - New MCP tool: boolean_op for agent-driven shape composition - 26 tests passing (5 boolean, 6 history, 15 existing)
This commit is contained in:
@@ -169,6 +169,24 @@ pub struct DeleteDrawingElementParam {
|
||||
pub id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct BooleanOpParam {
|
||||
#[schemars(description = "Session ID to target. If omitted, uses the active session.")]
|
||||
pub session_id: Option<String>,
|
||||
#[schemars(description = "Boolean operation: union, intersection, difference, or xor")]
|
||||
pub operation: String,
|
||||
#[schemars(description = "IDs of drawing elements to combine (minimum 2)")]
|
||||
pub element_ids: Vec<String>,
|
||||
#[schemars(description = "If true, delete source elements after combining")]
|
||||
pub consume: Option<bool>,
|
||||
#[schemars(description = "Fill color for result as hex e.g. '#ff0000'")]
|
||||
pub fill: Option<String>,
|
||||
#[schemars(description = "Stroke color for result as hex e.g. '#ffffff'")]
|
||||
pub stroke_color: Option<String>,
|
||||
#[schemars(description = "Stroke width for result in pixels")]
|
||||
pub stroke_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct AgCanvasServer {
|
||||
ws_url: String,
|
||||
@@ -185,7 +203,7 @@ impl AgCanvasServer {
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "List all open sessions/tabs in agcanvas. Returns session IDs, names, creator info, description, timestamps, and whether they have SVG or drawing content loaded. Supports sorting."
|
||||
description = "List all open sessions/tabs in Augmented Canvas. Returns session IDs, names, creator info, description, timestamps, and whether they have SVG or drawing content loaded. Supports sorting."
|
||||
)]
|
||||
async fn list_sessions(
|
||||
&self,
|
||||
@@ -203,7 +221,7 @@ impl AgCanvasServer {
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Create a new session/tab in agcanvas. The session is created by an agent. Returns the created session with its ID and metadata."
|
||||
description = "Create a new session/tab in Augmented Canvas. The session is created by an agent. Returns the created session with its ID and metadata."
|
||||
)]
|
||||
async fn create_session(
|
||||
&self,
|
||||
@@ -472,6 +490,37 @@ impl AgCanvasServer {
|
||||
self.call_agcanvas(&request).await
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Perform a boolean operation (union, intersection, difference, xor) on two or more drawing elements. Combines filled shapes into a new path element. Only works on Rectangle and Ellipse shapes."
|
||||
)]
|
||||
async fn boolean_op(
|
||||
&self,
|
||||
Parameters(params): Parameters<BooleanOpParam>,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
let mut request = serde_json::json!({
|
||||
"type": "BooleanOp",
|
||||
"operation": params.operation,
|
||||
"element_ids": params.element_ids,
|
||||
});
|
||||
let obj = request.as_object_mut().unwrap();
|
||||
if let Some(v) = params.session_id {
|
||||
obj.insert("session_id".into(), v.into());
|
||||
}
|
||||
if let Some(v) = params.consume {
|
||||
obj.insert("consume".into(), v.into());
|
||||
}
|
||||
if let Some(v) = params.fill {
|
||||
obj.insert("fill".into(), v.into());
|
||||
}
|
||||
if let Some(v) = params.stroke_color {
|
||||
obj.insert("stroke_color".into(), v.into());
|
||||
}
|
||||
if let Some(v) = params.stroke_width {
|
||||
obj.insert("stroke_width".into(), v.into());
|
||||
}
|
||||
self.call_agcanvas(&request).await
|
||||
}
|
||||
|
||||
#[tool(description = "Delete a drawing element by its ID.")]
|
||||
async fn delete_drawing_element(
|
||||
&self,
|
||||
@@ -517,7 +566,7 @@ impl AgCanvasServer {
|
||||
let msg = parsed
|
||||
.get("message")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("Unknown error from agcanvas");
|
||||
.unwrap_or("Unknown error from Augmented Canvas");
|
||||
return Ok(CallToolResult::error(vec![Content::text(msg)]));
|
||||
}
|
||||
|
||||
@@ -526,7 +575,7 @@ impl AgCanvasServer {
|
||||
Ok(CallToolResult::success(vec![Content::text(pretty)]))
|
||||
}
|
||||
Err(e) => Ok(CallToolResult::error(vec![Content::text(format!(
|
||||
"Failed to communicate with agcanvas: {}. Make sure agcanvas is running.",
|
||||
"Failed to communicate with Augmented Canvas: {}. Make sure Augmented Canvas is running.",
|
||||
e
|
||||
))])),
|
||||
}
|
||||
@@ -538,10 +587,10 @@ impl ServerHandler for AgCanvasServer {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
instructions: Some(
|
||||
"agcanvas MCP server — connects to agcanvas desktop app to query SVG designs, \
|
||||
"Augmented Canvas MCP server — connects to the Augmented Canvas desktop app to query SVG designs, \
|
||||
element trees, and user-drawn shapes. Use describe_canvas to understand the \
|
||||
current design, get_element_tree for structured data, and generate_code for \
|
||||
code stubs. Requires agcanvas to be running."
|
||||
code stubs. Requires Augmented Canvas to be running."
|
||||
.into(),
|
||||
),
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
|
||||
Reference in New Issue
Block a user