forked from 1b-agent/opencadstudio-upstream
Accelerate native MCP workflows
This commit is contained in:
1 parent
0e4f755e6f
commit
cb88f2ded6
6 files changed
+1291
-98
No files matched your search
+180
-27
@@ -138,9 +138,9 @@ fn v3(v: acadrust::types::Vector3) -> Value {
|
||||
json!([v.x, v.y, v.z])
|
||||
}
|
||||
|
||||
/// One entity as JSON: handle, type, layer, plus basic geometry for the common
|
||||
/// types (others carry only the common fields).
|
||||
fn entity_json(e: &acadrust::EntityType) -> Value {
|
||||
/// One entity as JSON. Summary mode carries identity only, geometry adds the
|
||||
/// entity's defining values, and full also includes its world bounds.
|
||||
fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value {
|
||||
use acadrust::EntityType as E;
|
||||
let c = e.common();
|
||||
let mut obj = json!({
|
||||
@@ -148,6 +148,9 @@ fn entity_json(e: &acadrust::EntityType) -> Value {
|
||||
"type": crate::entities::names::ui_name(e),
|
||||
"layer": c.layer,
|
||||
});
|
||||
if detail == "summary" {
|
||||
return obj;
|
||||
}
|
||||
let map = obj.as_object_mut().expect("json object");
|
||||
match e {
|
||||
E::Line(l) => {
|
||||
@@ -194,9 +197,41 @@ fn entity_json(e: &acadrust::EntityType) -> Value {
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if detail == "full" {
|
||||
let bounds = e.as_entity().bounding_box();
|
||||
map.insert("bounds".into(), json!({
|
||||
"min":[bounds.min.x,bounds.min.y,bounds.min.z],
|
||||
"max":[bounds.max.x,bounds.max.y,bounds.max.z]
|
||||
}));
|
||||
}
|
||||
obj
|
||||
}
|
||||
|
||||
fn request_point(req: &Value, key: &str) -> Option<[f64; 2]> {
|
||||
let values = req[key].as_array()?;
|
||||
if !(2..=3).contains(&values.len()) {
|
||||
return None;
|
||||
}
|
||||
let x = values[0].as_f64()?;
|
||||
let y = values[1].as_f64()?;
|
||||
(x.is_finite() && y.is_finite()).then_some([x, y])
|
||||
}
|
||||
|
||||
fn request_handle(value: &Value) -> Option<acadrust::Handle> {
|
||||
value.as_str()
|
||||
.and_then(|value| u64::from_str_radix(value.trim_start_matches("0x"), 16).ok())
|
||||
.map(acadrust::Handle::new)
|
||||
}
|
||||
|
||||
fn projected_fields(mut entity: Value, fields: Option<&Vec<Value>>) -> Value {
|
||||
let Some(fields) = fields else { return entity };
|
||||
let Some(source) = entity.as_object_mut() else { return entity };
|
||||
let keep: std::collections::HashSet<&str> =
|
||||
fields.iter().filter_map(Value::as_str).collect();
|
||||
source.retain(|key, _| key == "handle" || keep.contains(key.as_str()));
|
||||
entity
|
||||
}
|
||||
|
||||
impl OpenCADStudio {
|
||||
/// Handle one JSON request line and return the JSON response.
|
||||
#[cfg(any(test, not(target_arch = "wasm32")))]
|
||||
@@ -445,38 +480,156 @@ impl OpenCADStudio {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List entities (handle, type, layer, basic geometry), optionally filtered
|
||||
/// by `type` and/or `layer`, capped by `limit` (default 1000).
|
||||
/// Query entities by identity, metadata and exact plane-curve relationships.
|
||||
fn entity_query(&self, req: &Value) -> Value {
|
||||
let i = self.active_tab;
|
||||
let tab = &self.tabs[i];
|
||||
if let Some(pair) = req["intersections"].as_array() {
|
||||
if pair.len() != 2 {
|
||||
return err("query intersections expects exactly two handles");
|
||||
}
|
||||
let Some(first) = request_handle(&pair[0]) else {
|
||||
return err("query intersections contains an invalid first handle");
|
||||
};
|
||||
let Some(second) = request_handle(&pair[1]) else {
|
||||
return err("query intersections contains an invalid second handle");
|
||||
};
|
||||
let Some(first_entity) = tab.scene.document.get_entity(first) else {
|
||||
return err("query intersections first entity does not exist");
|
||||
};
|
||||
let Some(second_entity) = tab.scene.document.get_entity(second) else {
|
||||
return err("query intersections second entity does not exist");
|
||||
};
|
||||
let Some(first_curve) = crate::entities::curve::entity_curve_xy(first_entity) else {
|
||||
return err("query intersections first entity is not a planar curve");
|
||||
};
|
||||
let Some(second_curve) = crate::entities::curve::entity_curve_xy(second_entity) else {
|
||||
return err("query intersections second entity is not a planar curve");
|
||||
};
|
||||
let crossings = cadkernel::geom2d::intersect(
|
||||
&first_curve,
|
||||
&second_curve,
|
||||
cadkernel::geom2d::Tolerance::default(),
|
||||
);
|
||||
return json!({
|
||||
"ok":true,
|
||||
"document_id":tab.id,
|
||||
"geometry_revision":tab.scene.geometry_epoch,
|
||||
"handles":[format!("{:X}",first.value()),format!("{:X}",second.value())],
|
||||
"count":crossings.len(),
|
||||
"intersections":crossings.into_iter().map(|crossing|json!({
|
||||
"point":[crossing.point[0],crossing.point[1]],
|
||||
"parameter_first":crossing.t_a,
|
||||
"parameter_second":crossing.t_b
|
||||
})).collect::<Vec<_>>()
|
||||
});
|
||||
}
|
||||
|
||||
let type_filter = req["type"].as_str();
|
||||
let layer_filter = req["layer"].as_str();
|
||||
let limit = req["limit"].as_u64().unwrap_or(1000).min(10000) as usize;
|
||||
let offset = req["offset"].as_u64().unwrap_or(0);
|
||||
|
||||
let mut entities = Vec::new();
|
||||
let mut matched = 0u64;
|
||||
for e in self.tabs[i].scene.document.entities() {
|
||||
if let Some(tf) = type_filter {
|
||||
if !crate::entities::names::ui_name(e).eq_ignore_ascii_case(tf) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some(lf) = layer_filter {
|
||||
if e.common().layer != lf {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
matched += 1;
|
||||
if matched > offset && entities.len() < limit {
|
||||
entities.push(entity_json(e));
|
||||
}
|
||||
let handles: Option<std::collections::HashSet<u64>> = req["handles"]
|
||||
.as_array()
|
||||
.map(|values| values.iter().filter_map(request_handle).map(|h| h.value()).collect())
|
||||
.or_else(|| request_handle(&req["handle"])
|
||||
.map(|handle| std::iter::once(handle.value()).collect()));
|
||||
let near = request_point(req, "near");
|
||||
let contains = request_point(req, "contains_point");
|
||||
let bounds = req["bounds"].as_array().and_then(|values| {
|
||||
(values.len() == 4).then(|| Some([
|
||||
values[0].as_f64()?, values[1].as_f64()?,
|
||||
values[2].as_f64()?, values[3].as_f64()?,
|
||||
])).flatten()
|
||||
});
|
||||
if req.get("handles").is_some() && handles.as_ref().is_some_and(|parsed| {
|
||||
parsed.len() != req["handles"].as_array().map_or(0, Vec::len)
|
||||
}) {
|
||||
return err("query handles contains an invalid hexadecimal handle");
|
||||
}
|
||||
if req.get("handle").is_some() && request_handle(&req["handle"]).is_none() {
|
||||
return err("query handle must be hexadecimal");
|
||||
}
|
||||
if req.get("near").is_some() && near.is_none() {
|
||||
return err("query near expects two or three finite coordinates");
|
||||
}
|
||||
if req.get("contains_point").is_some() && contains.is_none() {
|
||||
return err("query contains_point expects two or three finite coordinates");
|
||||
}
|
||||
if req.get("bounds").is_some() && bounds.is_none_or(|bounds| {
|
||||
!bounds.iter().all(|value| value.is_finite())
|
||||
|| bounds[0] > bounds[2] || bounds[1] > bounds[3]
|
||||
}) {
|
||||
return err("query bounds expects finite [min_x,min_y,max_x,max_y]");
|
||||
}
|
||||
let detail = req["detail"].as_str().unwrap_or("geometry");
|
||||
if !matches!(detail, "summary" | "geometry" | "full") {
|
||||
return err("query detail must be summary, geometry or full");
|
||||
}
|
||||
let limit = req["limit"].as_u64().unwrap_or(1000).min(10000) as usize;
|
||||
let offset = req["offset"].as_u64().unwrap_or(0) as usize;
|
||||
|
||||
let mut matched = Vec::new();
|
||||
for e in tab.scene.document.entities() {
|
||||
if handles.as_ref().is_some_and(|handles| {
|
||||
!handles.contains(&e.common().handle.value())
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
if type_filter.is_some_and(|value| {
|
||||
!crate::entities::names::ui_name(e).eq_ignore_ascii_case(value)
|
||||
}) || layer_filter.is_some_and(|value| e.common().layer != value) {
|
||||
continue;
|
||||
}
|
||||
if let Some(bounds) = bounds {
|
||||
let entity_bounds = e.as_entity().bounding_box();
|
||||
if entity_bounds.max.x < bounds[0] || entity_bounds.max.y < bounds[1]
|
||||
|| entity_bounds.min.x > bounds[2] || entity_bounds.min.y > bounds[3]
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let curve = (near.is_some() || contains.is_some())
|
||||
.then(|| crate::entities::curve::entity_curve_xy(e)).flatten();
|
||||
if let Some(point) = contains {
|
||||
let Some(curve) = curve.as_ref().filter(|curve| curve.is_closed()) else {
|
||||
continue;
|
||||
};
|
||||
if !cadkernel::geom2d::contains(
|
||||
std::slice::from_ref(curve), point,
|
||||
cadkernel::geom2d::Tolerance::default(),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
let nearest = near.and_then(|point| curve.as_ref()
|
||||
.map(|curve| cadkernel::geom2d::closest_point(curve, point)));
|
||||
if near.is_some() && nearest.is_none() {
|
||||
continue;
|
||||
}
|
||||
let mut entity = entity_json(e, detail);
|
||||
if let Some(nearest) = nearest {
|
||||
let object = entity.as_object_mut().expect("entity JSON object");
|
||||
object.insert("distance".into(), json!(nearest.distance));
|
||||
object.insert("closest_point".into(),
|
||||
json!([nearest.point[0], nearest.point[1]]));
|
||||
object.insert("parameter".into(), json!(nearest.t));
|
||||
}
|
||||
matched.push((nearest.map(|nearest| nearest.distance), entity));
|
||||
}
|
||||
if near.is_some() {
|
||||
matched.sort_by(|left, right| left.0.partial_cmp(&right.0)
|
||||
.unwrap_or(std::cmp::Ordering::Equal));
|
||||
}
|
||||
let count = matched.len();
|
||||
let fields = req["fields"].as_array();
|
||||
let entities: Vec<Value> = matched.into_iter().skip(offset).take(limit)
|
||||
.map(|(_, entity)| projected_fields(entity, fields)).collect();
|
||||
json!({
|
||||
"ok": true,
|
||||
"count": matched,
|
||||
"document_id":tab.id,
|
||||
"geometry_revision":tab.scene.geometry_epoch,
|
||||
"count": count,
|
||||
"returned": entities.len(),
|
||||
"next_offset": if offset + (entities.len() as u64) < matched { Some(offset + entities.len() as u64) } else { None },
|
||||
"next_offset": (offset + entities.len() < count).then_some(offset + entities.len()),
|
||||
"entities": entities,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user