mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 13:26:49 -06:00
Workspace (geom, dxf, color, doc, fonts, render, io, engine, ui-egui, mcp; apps cadcraft, cadcraft-cli, cadcraft-web; xtask), AutoCAD-for-Mac style UI with code-drawn icons, command line with prompts/keywords/AutoComplete, draw and modify commands, object snaps, polar/ortho, selection, undo, layers and properties palettes, DXF read/write, SVG/PNG export, MCP server, JSON control channel, CLI, wasm build, README, ROADMAP, AGENTS.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
6.5 KiB
Rust
88 lines
6.5 KiB
Rust
//! MCP tool definitions and dispatch.
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::backend::Backend;
|
|
|
|
pub fn tool_definitions(has_ui: bool) -> Value {
|
|
let mut tools = vec![
|
|
json!({"name": "command_line", "description": "Type one line at the CADCraft command line, exactly as a user would at an AutoCAD-style prompt. Starts commands by name or alias (LINE, L, CIRCLE, C, PLINE, RECTANG, MOVE, TRIM, OFFSET, ZOOM…) and answers the active prompt: points `x,y`, relative `@dx,dy`, polar `@dist<angle`, distances, keywords (e.g. `c` for Close), empty text = Enter. Spaces act as Enter, so `circle 0,0 5` works in one call. Returns the new prompt and command-line output.",
|
|
"inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}),
|
|
json!({"name": "script", "description": "Run a multi-line script (like an AutoCAD .scr file): one or more inputs per line, blank line = Enter.",
|
|
"inputSchema": {"type": "object", "properties": {"text": {"type": "string"}}, "required": ["text"]}}),
|
|
json!({"name": "execute", "description": "Run any command with JSON parameters (no prompts or dialogs). Use list_commands to find ids and parameter docs. Examples: line {points:[[0,0],[10,0]]}, circle {center:[0,0], radius:5}, layer.new {name:\"Walls\", color:\"red\", current:true}, move {handles:[\"10A\"], delta:[5,0]}, zoom {mode:\"extents\"}.",
|
|
"inputSchema": {"type": "object", "properties": {"command": {"type": "string"}, "params": {"type": "object"}}, "required": ["command"]}}),
|
|
json!({"name": "list_commands", "description": "List commands with ids, labels, menu paths, aliases and parameter docs. Optional substring filter.",
|
|
"inputSchema": {"type": "object", "properties": {"filter": {"type": "string"}}}}),
|
|
json!({"name": "inspect_drawing", "description": "Summary of the active drawing: entity counts, extents, layers, styles, blocks, layouts, selection, undo history and (optionally) entities with handles and geometry.",
|
|
"inputSchema": {"type": "object", "properties": {"entities": {"type": "boolean"}, "limit": {"type": "integer"}}}}),
|
|
json!({"name": "query_entities", "description": "Entities filtered by type (Line, Circle, Polyline…), layer and/or a window [[x0,y0],[x1,y1]].",
|
|
"inputSchema": {"type": "object", "properties": {"type": {"type": "string"}, "layer": {"type": "string"}, "window": {"type": "array"}, "limit": {"type": "integer"}}}}),
|
|
json!({"name": "render", "description": "Render the active drawing to a PNG (fitted to extents by default) and return it as an image.",
|
|
"inputSchema": {"type": "object", "properties": {"width": {"type": "integer"}, "height": {"type": "integer"}, "fit": {"type": "boolean"}, "path": {"type": "string"}}}}),
|
|
json!({"name": "new_drawing", "description": "Create a new drawing (imperial by default, or metric).",
|
|
"inputSchema": {"type": "object", "properties": {"metric": {"type": "boolean"}}}}),
|
|
json!({"name": "open", "description": "Open a DXF file.", "inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}, "required": ["path"]}}),
|
|
json!({"name": "save", "description": "Save the drawing (DXF; .svg/.png export by extension).", "inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}}}),
|
|
json!({"name": "cancel", "description": "Press Escape: cancel the running command (or clear the selection).", "inputSchema": {"type": "object", "properties": {}}}),
|
|
];
|
|
if has_ui {
|
|
tools.push(json!({"name": "screenshot", "description": "Screenshot the CADCraft window (PNG).", "inputSchema": {"type": "object", "properties": {"path": {"type": "string"}}}}));
|
|
tools.push(json!({"name": "ui_inspect", "description": "UI state: panels, canvas rect, view, cursor and snap, performance.", "inputSchema": {"type": "object", "properties": {}}}));
|
|
tools.push(json!({"name": "ui_click", "description": "Click in the drawing area at world coordinates (or screen with space:\"screen\"). Right button = Enter.", "inputSchema": {"type": "object", "properties": {"x": {"type": "number"}, "y": {"type": "number"}, "space": {"type": "string"}, "button": {"type": "string"}}, "required": ["x", "y"]}}));
|
|
}
|
|
json!({ "tools": tools })
|
|
}
|
|
|
|
fn text(v: &Value) -> Value {
|
|
json!({"content": [{"type": "text", "text": serde_json::to_string_pretty(v).unwrap_or_default()}]})
|
|
}
|
|
|
|
fn err_text(e: &str) -> Value {
|
|
json!({"content": [{"type": "text", "text": e}], "isError": true})
|
|
}
|
|
|
|
pub fn call_tool(b: &mut dyn Backend, name: &str, args: &Value) -> Value {
|
|
let a = if args.is_object() { args.clone() } else { json!({}) };
|
|
let r = match name {
|
|
"command_line" => b.call("cmdline.input", a),
|
|
"script" => b.call("cmdline.script", a),
|
|
"execute" => b.call(
|
|
"engine.execute",
|
|
json!({"command": a.get("command").cloned().unwrap_or(Value::Null), "params": a.get("params").cloned().unwrap_or(json!({}))}),
|
|
),
|
|
"list_commands" => b.call("engine.commands", json!({})).map(|v| {
|
|
let f = a.get("filter").and_then(Value::as_str).map(str::to_ascii_lowercase);
|
|
match (v, f) {
|
|
(Value::Array(items), Some(f)) => {
|
|
Value::Array(items.into_iter().filter(|c| c.to_string().to_ascii_lowercase().contains(&f)).collect())
|
|
}
|
|
(v, _) => v,
|
|
}
|
|
}),
|
|
"inspect_drawing" => b.call("drawing.inspect", a),
|
|
"query_entities" => b.call("engine.execute", json!({"command": "entities", "params": a})),
|
|
"new_drawing" => b.call("engine.execute", json!({"command": "new", "params": a})),
|
|
"open" => b.call("app.open", a),
|
|
"save" => b.call("app.save", a),
|
|
"cancel" => b.call("cmdline.key", json!({"key": "escape"})),
|
|
"screenshot" => b.call("ui.screenshot", a),
|
|
"ui_inspect" => b.call("ui.inspect", json!({})),
|
|
"ui_click" => b.call("ui.pointer", a),
|
|
"render" => {
|
|
return match b.call("ui.render", a) {
|
|
Ok(v) => match v.get("pngBase64").and_then(Value::as_str) {
|
|
Some(data) => json!({"content": [{"type": "image", "data": data, "mimeType": "image/png"}]}),
|
|
None => text(&v),
|
|
},
|
|
Err(e) => err_text(&e),
|
|
};
|
|
}
|
|
other => return err_text(&format!("unknown tool `{other}`")),
|
|
};
|
|
match r {
|
|
Ok(v) => text(&v),
|
|
Err(e) => err_text(&e),
|
|
}
|
|
}
|