forked from 1b-agent/opencadstudio-upstream
feat(mcp): grips, grip_drag and click
grips lists the selection's grips; grip_drag moves one to a world point through the viewport's own grip gesture with snaps, ortho and polar off; click presses the left button where a world point shows, optionally with Ctrl and/or Shift held. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d3033caf24
commit
43bd5aa427
4 files changed
+150
-1
No files matched your search
@@ -733,6 +733,119 @@ impl OpenCADStudio {
|
||||
self.refresh_properties();
|
||||
json!({"ok":true,"sections":self.tabs[self.active_tab].properties.sections.iter().map(|s|json!({"title":s.title,"properties":s.props.iter().map(property_json).collect::<Vec<_>>()})).collect::<Vec<_>>()})
|
||||
}
|
||||
/// The grips of the current selection, as the viewport shows them.
|
||||
pub(super) fn control_grips(&self) -> Value {
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
let grips = tab
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.zip(&tab.selected_grips)
|
||||
.map(|(owner, grip)| {
|
||||
json!({
|
||||
"handle": format!("{:X}", owner.value()),
|
||||
"grip_id": grip.id,
|
||||
"point": [grip.world.x, grip.world.y, grip.world.z],
|
||||
"shape": format!("{:?}", grip.shape),
|
||||
"moves_entity": grip.is_midpoint,
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
json!({"ok": true, "grips": grips})
|
||||
}
|
||||
|
||||
/// Drag one grip of an entity to a world point through the viewport's
|
||||
/// own grip gesture: select the entity, engage the grip, move, place.
|
||||
/// Object snaps, grid, ortho, polar and tracking are off for the move so
|
||||
/// the grip lands on `point`.
|
||||
pub(super) fn control_grip_drag(&mut self, req: &Value) -> Result<Task<Message>, Value> {
|
||||
let i = self.active_tab;
|
||||
let target = handle(req)?;
|
||||
let grip_id = req["grip_id"]
|
||||
.as_u64()
|
||||
.ok_or_else(|| failure("invalid_request", "Missing grip_id"))? as usize;
|
||||
let to = point(req)?;
|
||||
self.tabs[i].scene.deselect_all();
|
||||
self.tabs[i].scene.select_entity(target, false);
|
||||
self.refresh_properties();
|
||||
let (world, moves_entity) = self.tabs[i]
|
||||
.selected_grip_handles
|
||||
.iter()
|
||||
.zip(&self.tabs[i].selected_grips)
|
||||
.find(|(owner, grip)| **owner == target && grip.id == grip_id)
|
||||
.map(|(_, grip)| (grip.world, grip.is_midpoint))
|
||||
.ok_or_else(|| failure("unknown_grip", "Read grips for the entity's grip ids"))?;
|
||||
let edit = self.grip_edit_for_hit(i, target, grip_id, moves_entity, world);
|
||||
self.tabs[i].active_grip = Some(edit);
|
||||
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
|
||||
let tile = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
|
||||
let local = iced::Rectangle {
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
..iced::Rectangle::default()
|
||||
};
|
||||
let Some(screen) = self.tabs[i].scene.camera.borrow().project(to, local) else {
|
||||
self.cancel_active_grip_edit();
|
||||
return Err(failure("invalid_point", "The point is not in view"));
|
||||
};
|
||||
let saved = (
|
||||
self.snapper.snap_enabled,
|
||||
self.snapper.snap3d_enabled,
|
||||
self.snapper.grid_snap_on,
|
||||
self.snapper.otrack_enabled,
|
||||
self.ortho_mode,
|
||||
self.polar_mode,
|
||||
);
|
||||
self.snapper.snap_enabled = false;
|
||||
self.snapper.snap3d_enabled = false;
|
||||
self.snapper.grid_snap_on = false;
|
||||
self.snapper.otrack_enabled = false;
|
||||
self.ortho_mode = false;
|
||||
self.polar_mode = false;
|
||||
let _ = self.on_viewport_move(iced::Point::new(screen.x + tile.x, screen.y + tile.y));
|
||||
(
|
||||
self.snapper.snap_enabled,
|
||||
self.snapper.snap3d_enabled,
|
||||
self.snapper.grid_snap_on,
|
||||
self.snapper.otrack_enabled,
|
||||
self.ortho_mode,
|
||||
self.polar_mode,
|
||||
) = saved;
|
||||
Ok(self.commit_active_grip_edit())
|
||||
}
|
||||
|
||||
/// Click the viewport where `point` (world) shows, as the left button
|
||||
/// would, holding Ctrl and/or Shift when `modifiers` names them.
|
||||
pub(super) fn control_click(&mut self, req: &Value) -> Result<Task<Message>, Value> {
|
||||
let i = self.active_tab;
|
||||
let at = point(req)?;
|
||||
let modifiers = req["modifiers"].as_str().unwrap_or("").to_ascii_lowercase();
|
||||
let (vw, vh) = self.tabs[i].scene.selection.borrow().view.vp_size;
|
||||
let tile = self.tabs[i].scene.active_model_tile_bounds(vw, vh);
|
||||
let local = iced::Rectangle {
|
||||
width: tile.width,
|
||||
height: tile.height,
|
||||
..iced::Rectangle::default()
|
||||
};
|
||||
let screen = self.tabs[i]
|
||||
.scene
|
||||
.camera
|
||||
.borrow()
|
||||
.project(at, local)
|
||||
.ok_or_else(|| failure("invalid_point", "The point is not in view"))?;
|
||||
let saved = (self.ctrl_down, self.shift_down);
|
||||
self.ctrl_down = modifiers.contains("ctrl");
|
||||
self.shift_down = modifiers.contains("shift");
|
||||
let cursor = iced::Point::new(screen.x + tile.x, screen.y + tile.y);
|
||||
let tasks = [
|
||||
self.update(Message::ViewportMove(cursor)),
|
||||
self.update(Message::ViewportLeftPress),
|
||||
self.update(Message::ViewportLeftRelease),
|
||||
];
|
||||
(self.ctrl_down, self.shift_down) = saved;
|
||||
Ok(Task::batch(tasks))
|
||||
}
|
||||
|
||||
pub(super) fn control_set_property(&mut self, req: &Value) -> Result<Task<Message>, Value> {
|
||||
self.refresh_properties();
|
||||
let field = string(req, "field")?;
|
||||
|
||||
@@ -460,6 +460,7 @@ impl OpenCADStudio {
|
||||
| "properties"
|
||||
| "measure"
|
||||
| "snap"
|
||||
| "grips"
|
||||
| "query"
|
||||
| "records"
|
||||
| "record_schema"
|
||||
@@ -742,6 +743,7 @@ impl OpenCADStudio {
|
||||
None => json!({"ok":true,"snap":null}),
|
||||
}
|
||||
}
|
||||
"grips" => self.control_grips(),
|
||||
"xdata_get" => self.xdata_read(&req),
|
||||
"get_selection" => self.control_get_selection(),
|
||||
"text_search" => self.control_text_search(&req).unwrap_or_else(|e| e),
|
||||
@@ -980,6 +982,8 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
"property" => self.control_set_property(req)?,
|
||||
"grip_drag" => self.control_grip_drag(req)?,
|
||||
"click" => self.control_click(req)?,
|
||||
"set_properties" => self.control_set_record_properties(req)?,
|
||||
"action" => self.control_ui_action(req)?,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
|
||||
+6
-1
@@ -29,7 +29,7 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
|
||||
/// changes, so schema drift (new params, renamed tools) is always a conscious,
|
||||
/// reviewed edit — and clients can detect a stale bridge by comparing digests.
|
||||
#[cfg(test)]
|
||||
const TOOL_SCHEMA_DIGEST: &str = "bbecd61a9295ecd8e3fefe6a223252107d80d173da7544edacaa2880139683ad";
|
||||
const TOOL_SCHEMA_DIGEST: &str = "2ec01401ca96bc0574a9be41958d24dbd9c27a497dce80064c8afcca5258256e";
|
||||
const MAX_REQUEST: usize = 1_048_576;
|
||||
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
|
||||
const CACHE_TTL_MS: u64 = 3_600_000;
|
||||
@@ -52,6 +52,7 @@ const READ_OPS: &[&str] = &[
|
||||
"properties",
|
||||
"measure",
|
||||
"snap",
|
||||
"grips",
|
||||
"history",
|
||||
"commands",
|
||||
"events",
|
||||
@@ -75,6 +76,8 @@ const EXECUTE_OPS: &[&str] = &[
|
||||
"redo",
|
||||
"select",
|
||||
"property",
|
||||
"grip_drag",
|
||||
"click",
|
||||
"set_properties",
|
||||
"action",
|
||||
"embed_image",
|
||||
@@ -117,6 +120,8 @@ const BATCH_STEP_OPS: &[&str] = &[
|
||||
"redo",
|
||||
"select",
|
||||
"property",
|
||||
"grip_drag",
|
||||
"click",
|
||||
"set_properties",
|
||||
"action",
|
||||
"embed_image",
|
||||
|
||||
@@ -223,6 +223,31 @@ pub static PROPERTY: OpDef = OpDef {
|
||||
example: r#"{"op":"property","field":"color","value":1}"#,
|
||||
};
|
||||
|
||||
pub static GRIP_DRAG: OpDef = OpDef {
|
||||
name: "grip_drag",
|
||||
doc: "Drag one grip of an entity to a world point the way a grip pick-and-place in the viewport does. Read grips for the ids; object snaps, ortho and polar are off for the move.",
|
||||
batchable: true,
|
||||
params: &[
|
||||
req("handle", Ty::Handle, "Entity whose grip moves."),
|
||||
req("grip_id", Ty::Int, "Grip id from the grips read."),
|
||||
req("point", Ty::Point, "World point [x, y, z] the grip moves to."),
|
||||
],
|
||||
rules: &[],
|
||||
example: r#"{"op":"grip_drag","handle":"2A","grip_id":0,"point":[10,0,0]}"#,
|
||||
};
|
||||
|
||||
pub static CLICK: OpDef = OpDef {
|
||||
name: "click",
|
||||
doc: "Left-click the viewport where a world point shows, as the mouse would. modifiers holds ctrl and/or shift (e.g. Ctrl+click picks the solid of a composite under the cursor).",
|
||||
batchable: true,
|
||||
params: &[
|
||||
req("point", Ty::Point, "World point [x, y, z] to click on."),
|
||||
opt("modifiers", Ty::Str, "Keys held during the click: \"ctrl\", \"shift\" or \"ctrl+shift\"."),
|
||||
],
|
||||
rules: &[],
|
||||
example: r#"{"op":"click","point":[10,0,0],"modifiers":"ctrl"}"#,
|
||||
};
|
||||
|
||||
pub static SET_PROPERTIES: OpDef = OpDef {
|
||||
name: "set_properties",
|
||||
doc: "Atomically update record properties by RFC 6901 JSON pointer paths.",
|
||||
@@ -635,6 +660,8 @@ pub static OPS: &[&OpDef] = &[
|
||||
&REDO,
|
||||
&SELECT,
|
||||
&PROPERTY,
|
||||
&GRIP_DRAG,
|
||||
&CLICK,
|
||||
&SET_PROPERTIES,
|
||||
&ACTION,
|
||||
&EMBED_IMAGE,
|
||||
|
||||
Reference in new issue
Block a user