forked from 1b-agent/opencadstudio-upstream
feat(mcp): expose complete drawing records
This commit is contained in:
1 parent
89572e5ca9
commit
8f574c8388
14 files changed
+1492
-187
No files matched your search
Generated
+2
-2
@@ -79,7 +79,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.5.4"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=1e7808d719c387a9cf26850683991980bf82667f#1e7808d719c387a9cf26850683991980bf82667f"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=7b4c112bd7895fe9039514c21c5b5aa59eaf2525#7b4c112bd7895fe9039514c21c5b5aa59eaf2525"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
@@ -885,7 +885,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=304a375be1f73945af24e6e0f5b4664417a9d9e0#304a375be1f73945af24e6e0f5b4664417a9d9e0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=8efd43cb2f04577d20c3ee33ce5273bb49affa3d#8efd43cb2f04577d20c3ee33ce5273bb49affa3d"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
|
||||
+2
-2
@@ -31,8 +31,8 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
||||
rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1e7808d719c387a9cf26850683991980bf82667f", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "304a375be1f73945af24e6e0f5b4664417a9d9e0", features = ["acis", "offset"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8efd43cb2f04577d20c3ee33ce5273bb49affa3d", features = ["acis", "offset"] }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -14,8 +14,8 @@ bincode = "1.3"
|
||||
thiserror = "1"
|
||||
|
||||
# Host adapters convert ObjectId to acadrust handles; kernel handles stay local.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1e7808d719c387a9cf26850683991980bf82667f", features = ["serde"], optional = true }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "304a375be1f73945af24e6e0f5b4664417a9d9e0", features = ["acis", "offset"], optional = true }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"], optional = true }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "8efd43cb2f04577d20c3ee33ce5273bb49affa3d", features = ["acis", "offset"], optional = true }
|
||||
ocs_plugin_api = { path = "../ocs_plugin_api", optional = true, features = ["host"] }
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1e7808d719c387a9cf26850683991980bf82667f", optional = true, features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", optional = true, features = ["serde"] }
|
||||
|
||||
# Runtime IPC and serialization (host feature only).
|
||||
interprocess = { version = "2", optional = true }
|
||||
@@ -38,7 +38,7 @@ serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
cargo-lock = "11"
|
||||
# acadrust is scanned at build time to generate the embedded type registry.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1e7808d719c387a9cf26850683991980bf82667f", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1e7808d719c387a9cf26850683991980bf82667f", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
|
||||
bincode = "1.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
@@ -10,4 +10,4 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "1e7808d719c387a9cf26850683991980bf82667f", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7b4c112bd7895fe9039514c21c5b5aa59eaf2525", features = ["serde"] }
|
||||
@@ -13,8 +13,8 @@ The client must start that command over standard input/output. If it asks for th
|
||||
The server provides four tools:
|
||||
|
||||
- `ocs_sessions` finds running editor sessions and opens OpenCADStudio when none exists.
|
||||
- `ocs_read` reads document state, command manifests, entities, layers, properties, kernel measurements and spatial relationships, history, events, and operation status.
|
||||
- `ocs_execute` performs one operation or a sequential batch against the real editor.
|
||||
- `ocs_read` discovers capabilities and reads document state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events, and operation status.
|
||||
- `ocs_execute` performs one operation, an atomic record update, or a sequential batch against the real editor.
|
||||
- `ocs_capture` returns a bounded PNG of the drawing viewport or complete window.
|
||||
|
||||
The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `ocs_session_id` to the other tools, read the chosen session with `ocs_read`, then pass the returned document state into `ocs_execute`. For example, an undo request has this shape:
|
||||
@@ -31,6 +31,50 @@ The normal flow is to call `ocs_sessions`, pass its returned `session_id` as `oc
|
||||
}
|
||||
```
|
||||
|
||||
Call `ocs_read` with `op: "capabilities"` before unfamiliar work. It reports the command, geometry, transaction, capture, and database facilities supported by the running build. The `records.collections` list gives every available database collection, its record count, and whether it can be edited.
|
||||
|
||||
`op: "records"` returns every serializable property of entities, non-graphical objects, symbol-table records, the complete header, document metadata, and derived database views. Omitting `collection` returns the same collection manifest; use `collection: "all"` for a paged query across the complete database. Records have a stable collection plus a handle or name, a type, and `properties`. Entity records also include their display and file record type names.
|
||||
|
||||
Property filters and projections use RFC 6901 JSON Pointer paths relative to `properties`. Multiple `where` filters are combined with AND. Supported operators are `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `contains`, `starts_with`, `ends_with`, `in`, `exists`, and `not_exists`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
|
||||
"op": "records",
|
||||
"parameters": {
|
||||
"collection": "entities",
|
||||
"type": "Insert",
|
||||
"where": [
|
||||
{"path": "/common/layer", "value": "Equipment"},
|
||||
{"path": "/attributes/0/tag", "value": "COMPANY"}
|
||||
],
|
||||
"paths": ["/block_name", "/insert_point", "/attributes"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Use `set_properties` to replace one or more fields atomically. OCS verifies the record identity, JSON types, optional compare-and-set values, layer locks, document revision, and request ID before committing one undoable edit. Handles, ownership slots, table names, and other database identity fields are read-only; their dedicated editor commands preserve cross-record references.
|
||||
|
||||
```json
|
||||
{
|
||||
"ocs_session_id": "SESSION_FROM_OCS_SESSIONS",
|
||||
"request": {
|
||||
"op": "set_properties",
|
||||
"request_id": "attributes-1",
|
||||
"document_id": 1,
|
||||
"revision": 12,
|
||||
"collection": "entities",
|
||||
"handle": "2A",
|
||||
"updates": [
|
||||
{"path": "/attributes/0/value", "expected": "BPH", "value": "Updated"},
|
||||
{"path": "/insert_point/z", "value": 3.0}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The same operation edits entries in `objects`, every symbol table, `header`, and `summary_info`. Collections marked `mutable: false` are derived or storage metadata and remain queryable. Their source records must be changed so OCS can rebuild those views consistently.
|
||||
|
||||
To discover command syntax without reading application source, call `ocs_read` with `op: "commands"`. The unfiltered response lists commands and actions. Add `parameters: {"name": "PLINE"}` for a command's batch examples and interactive guidance.
|
||||
|
||||
A complete command can be sent with `run`. Prompt answers are separated by spaces, points use `x,y` or `x,y,z`, and option answers use their displayed token:
|
||||
@@ -67,7 +111,7 @@ Use `batch` when the steps are already known. OCS supplies each step with the st
|
||||
|
||||
Execute responses use `response_detail: "compact"` by default and return only the state needed for the next edit. Use `changed_entities` to receive the current geometry of affected handles in the same response, or `full` when the complete editor state is needed.
|
||||
|
||||
`ocs_read` query accepts exact handles, type/layer filters, field projection, world-XY bounds, nearest-curve ranking, closed-curve containment, and exact intersections between two planar curves. Nearest points, containment, curve length, area, and intersections are calculated by the geometry kernel:
|
||||
`ocs_read` query accepts exact handles, type/layer filters, field projection, world-XY bounds, nearest-curve ranking, closed-curve containment, and exact intersections between two planar curves. `detail: "full"` adds the complete serialized entity properties. Nearest points, containment, curve length, area, and intersections are calculated by the geometry kernel:
|
||||
|
||||
```json
|
||||
{
|
||||
|
||||
@@ -52,9 +52,12 @@ def legacy(server: Path) -> None:
|
||||
names = set(definitions)
|
||||
assert names == TOOLS, names
|
||||
execute_request = definitions["ocs_execute"]["inputSchema"]["properties"]["request"]
|
||||
assert len(execute_request["oneOf"]) == 15
|
||||
assert len(execute_request["oneOf"]) == 16
|
||||
assert execute_request["properties"]["steps"]["maxItems"] == 64
|
||||
assert execute_request["properties"]["cmd"]["examples"][0] == "LINE 0,0 10,10"
|
||||
assert "set_properties" in execute_request["properties"]["op"]["enum"]
|
||||
read_parameters = definitions["ocs_read"]["inputSchema"]["properties"]["parameters"]["properties"]
|
||||
assert {"collection", "where", "paths"} <= set(read_parameters)
|
||||
assert execute_request["properties"]["kind"]["enum"] == [
|
||||
"text", "token", "point", "entity", "structure", "selection", "enter"
|
||||
]
|
||||
|
||||
+215
-81
@@ -18,7 +18,7 @@
|
||||
use std::io::{BufRead, Write};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::OpenCADStudio;
|
||||
|
||||
@@ -138,6 +138,14 @@ fn v3(v: acadrust::types::Vector3) -> Value {
|
||||
json!([v.x, v.y, v.z])
|
||||
}
|
||||
|
||||
fn entity_type_matches(entity: &acadrust::EntityType, requested: &str) -> bool {
|
||||
if crate::entities::names::ui_name(entity).eq_ignore_ascii_case(requested) {
|
||||
return true;
|
||||
}
|
||||
let record_name = crate::entities::names::dxf_name(entity);
|
||||
record_name != "ENTITY" && record_name.eq_ignore_ascii_case(requested)
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -194,15 +202,30 @@ fn entity_json(e: &acadrust::EntityType, detail: &str) -> Value {
|
||||
}
|
||||
E::Insert(ins) => {
|
||||
map.insert("block".into(), json!(ins.block_name));
|
||||
map.insert("position".into(), v3(ins.insert_point));
|
||||
let attributes: serde_json::Map<String, Value> = ins
|
||||
.attributes
|
||||
.iter()
|
||||
.map(|attribute| (attribute.tag.clone(), json!(attribute.value)))
|
||||
.collect();
|
||||
map.insert("attributes".into(), Value::Object(attributes));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
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]
|
||||
}));
|
||||
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]
|
||||
}),
|
||||
);
|
||||
if let Ok(Value::Object(wrapper)) = serde_json::to_value(e) {
|
||||
if let Some((_, properties)) = wrapper.into_iter().next() {
|
||||
map.insert("properties".into(), properties);
|
||||
}
|
||||
}
|
||||
}
|
||||
obj
|
||||
}
|
||||
@@ -218,16 +241,24 @@ fn request_point(req: &Value, key: &str) -> Option<[f64; 2]> {
|
||||
}
|
||||
|
||||
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())
|
||||
value
|
||||
.as_str()
|
||||
.and_then(|value| {
|
||||
let value = value
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| value.strip_prefix("0X"))
|
||||
.unwrap_or(value);
|
||||
u64::from_str_radix(value, 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();
|
||||
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
|
||||
}
|
||||
@@ -244,7 +275,9 @@ impl OpenCADStudio {
|
||||
return err(error);
|
||||
}
|
||||
if matches!(response["status"].as_str(), Some("accepted" | "running")) {
|
||||
return self.control_request(json!({"op":"operation","request_id":id})).0;
|
||||
return self
|
||||
.control_request(json!({"op":"operation","request_id":id}))
|
||||
.0;
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -316,7 +349,9 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
let before = self.tabs[i].scene.document.entities().count();
|
||||
let error_revision = self.command_line.error_revision;
|
||||
if let Err(error) = self.run_headless(&cmd) { return err(error); }
|
||||
if let Err(error) = self.run_headless(&cmd) {
|
||||
return err(error);
|
||||
}
|
||||
if self.command_line.error_revision != error_revision {
|
||||
return err(self.command_line.last_error.clone().unwrap_or_default());
|
||||
}
|
||||
@@ -331,13 +366,14 @@ impl OpenCADStudio {
|
||||
}
|
||||
"entities" => self.entity_summary(),
|
||||
"query" => self.entity_query(&req),
|
||||
"records" => self.record_query(&req),
|
||||
"capabilities" => self.record_capabilities(),
|
||||
"layers" => {
|
||||
let i = self.active_tab;
|
||||
let offset = req["offset"].as_u64().unwrap_or(0) as usize;
|
||||
let limit = req["limit"].as_u64().unwrap_or(1000).min(10_000) as usize;
|
||||
let count = self.tabs[i].scene.document.layers.iter().count();
|
||||
let layers: Vec<Value> = self
|
||||
.tabs[i]
|
||||
let layers: Vec<Value> = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.layers
|
||||
@@ -397,8 +433,14 @@ impl OpenCADStudio {
|
||||
// By explicit handles (hex, as returned by `query`).
|
||||
if let Some(arr) = req["handles"].as_array() {
|
||||
for h in arr.iter().filter_map(|h| h.as_str()) {
|
||||
if let Ok(v) = u64::from_str_radix(h.trim_start_matches("0x"), 16) {
|
||||
self.tabs[i].scene.select_entity(acadrust::Handle::new(v), false);
|
||||
let h = h
|
||||
.strip_prefix("0x")
|
||||
.or_else(|| h.strip_prefix("0X"))
|
||||
.unwrap_or(h);
|
||||
if let Ok(v) = u64::from_str_radix(h, 16) {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.select_entity(acadrust::Handle::new(v), false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -410,11 +452,7 @@ impl OpenCADStudio {
|
||||
.scene
|
||||
.document
|
||||
.entities()
|
||||
.filter(|e| {
|
||||
type_filter.is_none_or(|t| {
|
||||
crate::entities::names::ui_name(e).eq_ignore_ascii_case(t)
|
||||
})
|
||||
})
|
||||
.filter(|e| type_filter.is_none_or(|t| entity_type_matches(e, t)))
|
||||
.filter(|e| layer_filter.is_none_or(|l| e.common().layer == l))
|
||||
.map(|e| e.common().handle)
|
||||
.collect();
|
||||
@@ -435,8 +473,7 @@ impl OpenCADStudio {
|
||||
return err("save: no \"path\" and the document has none");
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let result =
|
||||
self.save_tab_synchronously_protected(i, path.clone(), true);
|
||||
let result = self.save_tab_synchronously_protected(i, path.clone(), true);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let result = crate::io::save(&self.tabs[i].scene.document, &path)
|
||||
.map_err(crate::io::SaveFailure::other);
|
||||
@@ -461,19 +498,30 @@ impl OpenCADStudio {
|
||||
self.drive_headless_task(task)
|
||||
}
|
||||
|
||||
pub(super) fn drive_headless_task(&mut self, task: iced::Task<super::Message>) -> Result<(), String> {
|
||||
pub(super) fn drive_headless_task(
|
||||
&mut self,
|
||||
task: iced::Task<super::Message>,
|
||||
) -> Result<(), String> {
|
||||
use iced::futures::StreamExt;
|
||||
let mut streams = Vec::new();
|
||||
if let Some(stream) = iced_runtime::task::into_stream(task) { streams.push(stream); }
|
||||
if let Some(stream) = iced_runtime::task::into_stream(task) {
|
||||
streams.push(stream);
|
||||
}
|
||||
while let Some(stream) = streams.last_mut() {
|
||||
match iced::futures::executor::block_on(stream.next()) {
|
||||
Some(iced_runtime::Action::Output(message)) => {
|
||||
let next = self.update(message);
|
||||
if let Some(stream) = iced_runtime::task::into_stream(next) { streams.push(stream); }
|
||||
if let Some(stream) = iced_runtime::task::into_stream(next) {
|
||||
streams.push(stream);
|
||||
}
|
||||
}
|
||||
Some(iced_runtime::Action::Widget(_)) | Some(iced_runtime::Action::Tick) | Some(iced_runtime::Action::Reload) => {},
|
||||
Some(iced_runtime::Action::Widget(_))
|
||||
| Some(iced_runtime::Action::Tick)
|
||||
| Some(iced_runtime::Action::Reload) => {}
|
||||
Some(action) => return Err(format!("GUI runtime required for {action:?}")),
|
||||
None => { streams.pop(); }
|
||||
None => {
|
||||
streams.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
self.finish_all_pending_history();
|
||||
@@ -529,20 +577,36 @@ impl OpenCADStudio {
|
||||
let layer_filter = req["layer"].as_str();
|
||||
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()));
|
||||
.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()
|
||||
(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)
|
||||
}) {
|
||||
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() {
|
||||
@@ -554,10 +618,13 @@ impl OpenCADStudio {
|
||||
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]
|
||||
}) {
|
||||
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");
|
||||
@@ -569,39 +636,47 @@ impl OpenCADStudio {
|
||||
|
||||
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())
|
||||
}) {
|
||||
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) {
|
||||
if type_filter.is_some_and(|value| !entity_type_matches(e, 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]
|
||||
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();
|
||||
.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,
|
||||
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)));
|
||||
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;
|
||||
}
|
||||
@@ -609,20 +684,29 @@ impl OpenCADStudio {
|
||||
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(
|
||||
"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));
|
||||
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();
|
||||
let entities: Vec<Value> = matched
|
||||
.into_iter()
|
||||
.skip(offset)
|
||||
.take(limit)
|
||||
.map(|(_, entity)| projected_fields(entity, fields))
|
||||
.collect();
|
||||
json!({
|
||||
"ok": true,
|
||||
"document_id":tab.id,
|
||||
@@ -657,7 +741,10 @@ mod tests {
|
||||
fn layout_notice_skips_grid_camera_and_scene_builds() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
assert_eq!(app.automation_op(r#"{"op":"new"}"#)["ok"], true);
|
||||
assert_eq!(app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"], true);
|
||||
assert_eq!(
|
||||
app.automation_op(r#"{"op":"run","cmd":"LINE 0,0 10,10"}"#)["ok"],
|
||||
true
|
||||
);
|
||||
let i = app.active_tab;
|
||||
let scene = &mut app.tabs[i].scene;
|
||||
scene.document.add_layout("Review").unwrap();
|
||||
@@ -738,6 +825,44 @@ mod tests {
|
||||
assert_eq!(app.automation_op(r#"{"op":"run"}"#)["ok"], false);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_reference_query_exposes_instance_attributes() {
|
||||
use acadrust::entities::{AttributeEntity, EntityType, Insert};
|
||||
use acadrust::types::Vector3;
|
||||
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
let mut insert = Insert::new("A_CPT", Vector3::new(12.0, 34.0, 5.0));
|
||||
insert.common.layer = "Equipment".to_string();
|
||||
insert
|
||||
.attributes
|
||||
.push(AttributeEntity::simple("COMPANY", "BPH"));
|
||||
insert
|
||||
.attributes
|
||||
.push(AttributeEntity::simple("STATUS", "ACTIVE"));
|
||||
app.tabs[i].scene.add_entity(EntityType::Insert(insert));
|
||||
|
||||
let result = app
|
||||
.automation_op(r#"{"op":"query","type":"Insert","layer":"Equipment","detail":"full"}"#);
|
||||
assert_eq!(result["count"], 1);
|
||||
assert_eq!(result["entities"][0]["type"], "Block Reference");
|
||||
assert_eq!(result["entities"][0]["block"], "A_CPT");
|
||||
assert_eq!(
|
||||
result["entities"][0]["position"],
|
||||
serde_json::json!([12.0, 34.0, 5.0])
|
||||
);
|
||||
assert_eq!(result["entities"][0]["attributes"]["COMPANY"], "BPH");
|
||||
assert_eq!(result["entities"][0]["attributes"]["STATUS"], "ACTIVE");
|
||||
assert_eq!(
|
||||
result["entities"][0]["properties"]["attributes"][0]["value"],
|
||||
"BPH"
|
||||
);
|
||||
|
||||
let selected = app.automation_op(r#"{"op":"select","type":"Insert"}"#);
|
||||
assert_eq!(selected["selected"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ucs_interactive_inline_args() {
|
||||
// `UCS Z 90` must drive the interactive UCS command step-by-step (option
|
||||
@@ -789,9 +914,7 @@ mod tests {
|
||||
fn tilted_ucs_places_planar_entities_with_the_plane_normal() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
app.automation_op(
|
||||
r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#,
|
||||
);
|
||||
app.automation_op(r#"{"op":"run","cmd":"UCS 3POINT 0,0,0 1,0,0 0,0,1"}"#);
|
||||
app.automation_op(r#"{"op":"run","cmd":"CIRCLE 2,3 1"}"#);
|
||||
|
||||
let circle = app.tabs[app.active_tab]
|
||||
@@ -831,7 +954,10 @@ mod tests {
|
||||
h.linetype_scale
|
||||
);
|
||||
// No command should be left dangling.
|
||||
assert!(app.tabs[i].active_cmd.is_none(), "command must have finished");
|
||||
assert!(
|
||||
app.tabs[i].active_cmd.is_none(),
|
||||
"command must have finished"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -993,7 +1119,9 @@ mod tests {
|
||||
(20.0, 300.0),
|
||||
]
|
||||
};
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(path[0].0, path[0].1)));
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(
|
||||
path[0].0, path[0].1,
|
||||
)));
|
||||
let _ = app.update(Message::ViewportLeftPress);
|
||||
std::thread::sleep(std::time::Duration::from_millis(180));
|
||||
for &(x, y) in &path {
|
||||
@@ -1036,7 +1164,10 @@ mod tests {
|
||||
// Two-step: bare command then the value, like typing 1 + Enter
|
||||
// (feed_active_cmd is the same path the GUI submit offers first).
|
||||
let _ = app.run_command_line("PICKDRAG");
|
||||
assert!(app.tabs[app.active_tab].active_cmd.is_some(), "prompt must open");
|
||||
assert!(
|
||||
app.tabs[app.active_tab].active_cmd.is_some(),
|
||||
"prompt must open"
|
||||
);
|
||||
let _ = app.feed_active_cmd("1");
|
||||
assert!(app.pick_drag_rect, "PICKDRAG 1 via the prompt must switch");
|
||||
}
|
||||
@@ -1074,10 +1205,7 @@ mod tests {
|
||||
let _ = app.run_command_line("MATCHPROP");
|
||||
assert!(app.tabs[i].active_cmd.is_some(), "MATCHPROP must start");
|
||||
let _ = app.feed_command(StepInput::EntityPick(src_h, glam::DVec3::ZERO));
|
||||
let _ = app.feed_command(StepInput::SelectionComplete(vec![
|
||||
dst_text_h,
|
||||
dst_mtext_h,
|
||||
]));
|
||||
let _ = app.feed_command(StepInput::SelectionComplete(vec![dst_text_h, dst_mtext_h]));
|
||||
|
||||
let doc = &app.tabs[i].scene.document;
|
||||
match doc.get_entity(dst_text_h) {
|
||||
@@ -1129,7 +1257,10 @@ mod tests {
|
||||
app.opening.is_none(),
|
||||
"an already-open drawing must not start a load"
|
||||
);
|
||||
assert!(app.pending_opens.is_empty(), "and must not queue one either");
|
||||
assert!(
|
||||
app.pending_opens.is_empty(),
|
||||
"and must not queue one either"
|
||||
);
|
||||
|
||||
// The same file spelled differently (a `..` hop) is still the same file.
|
||||
let indirect = canon.parent().unwrap().join("..").join(
|
||||
@@ -1223,8 +1354,7 @@ mod tests {
|
||||
assert_eq!(saved["ok"], true, "{label}: {}", saved["error"]);
|
||||
let saved_again = app.automation_op(r#"{"op":"save"}"#);
|
||||
assert_eq!(
|
||||
saved_again["ok"],
|
||||
true,
|
||||
saved_again["ok"], true,
|
||||
"normal save: {}",
|
||||
saved_again["error"]
|
||||
);
|
||||
@@ -1242,10 +1372,8 @@ mod tests {
|
||||
#[test]
|
||||
fn save_then_open_round_trips() {
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ocs_automation_test_{}.dxf",
|
||||
std::process::id()
|
||||
));
|
||||
let path =
|
||||
std::env::temp_dir().join(format!("ocs_automation_test_{}.dxf", std::process::id()));
|
||||
let _ = std::fs::remove_file(&path);
|
||||
let p = path.to_string_lossy().replace('\\', "\\\\");
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
@@ -1275,7 +1403,7 @@ mod tests {
|
||||
app.tabs[0].scene.selection.borrow_mut().vp_size = (1920.0, 1080.0);
|
||||
app.tabs[0].scene.sync_tiles_from_panes(1920.0, 1080.0);
|
||||
}
|
||||
|
||||
|
||||
// Start PLINE
|
||||
let _ = app.update(Message::CommandInput("PLINE".to_string()));
|
||||
let _ = app.update(Message::CommandSubmit);
|
||||
@@ -1295,8 +1423,14 @@ mod tests {
|
||||
// Switch to arc: Option 1 - CommandOptionPick("A")
|
||||
let _ = app.update(Message::CommandOptionPick("A".to_string()));
|
||||
let _ = app.view(wid);
|
||||
println!("ENTITIES: {}", app.tabs[0].scene.document.entities().count());
|
||||
println!("CMD: {:?}", app.tabs[0].active_cmd.as_ref().map(|c| c.name()));
|
||||
println!(
|
||||
"ENTITIES: {}",
|
||||
app.tabs[0].scene.document.entities().count()
|
||||
);
|
||||
println!(
|
||||
"CMD: {:?}",
|
||||
app.tabs[0].active_cmd.as_ref().map(|c| c.name())
|
||||
);
|
||||
|
||||
// Move mouse!
|
||||
let _ = app.update(Message::ViewportMove(iced::Point::new(200.0, 100.0)));
|
||||
|
||||
+27
-12
@@ -2,7 +2,7 @@
|
||||
use super::{Message, OpenCADStudio};
|
||||
use crate::command::{InputKind, StepInput};
|
||||
use iced::Task;
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
use std::{collections::VecDeque, sync::OnceLock};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod transport;
|
||||
@@ -361,7 +361,7 @@ impl OpenCADStudio {
|
||||
"mtext_editor":self.mtext_editor.as_ref().map(|e|json!({"text":e.content.text(),"height":e.height,"style":e.style})),
|
||||
"text_editor":self.text_inline.is_some(),"event_cursor":self.control.serial,
|
||||
"operation":self.control.pending.as_ref().map(|p| &p.id),
|
||||
"capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"]
|
||||
"capabilities":["commands","command_manifest","step_input","batch","compact_results","entity_pick","structure_pick","selection","properties","records","record_filters","atomic_record_updates","layers","history","documents","events","capture","viewport_capture","measure","spatial_query"]
|
||||
})
|
||||
}
|
||||
|
||||
@@ -377,6 +377,8 @@ impl OpenCADStudio {
|
||||
| "properties"
|
||||
| "measure"
|
||||
| "query"
|
||||
| "records"
|
||||
| "capabilities"
|
||||
| "entities"
|
||||
| "layers"
|
||||
| "header"
|
||||
@@ -449,7 +451,9 @@ impl OpenCADStudio {
|
||||
return (
|
||||
failure(
|
||||
"unknown_command",
|
||||
format!("Unknown command {requested}; call commands without name to list commands"),
|
||||
format!(
|
||||
"Unknown command {requested}; call commands without name to list commands"
|
||||
),
|
||||
),
|
||||
Task::none(),
|
||||
);
|
||||
@@ -511,7 +515,7 @@ impl OpenCADStudio {
|
||||
"Supply a unique request_id (maximum 128 bytes)",
|
||||
),
|
||||
Task::none(),
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
if let Some((_, old, result)) = self.control.completed.iter().find(|(i, _, _)| i == &id)
|
||||
@@ -773,6 +777,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
"property" => self.control_set_property(req)?,
|
||||
"set_properties" => self.control_set_record_properties(req)?,
|
||||
"action" => self.control_ui_action(req)?,
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
"save" => {
|
||||
@@ -814,6 +819,12 @@ impl OpenCADStudio {
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn set_control_result(&mut self, value: Value) {
|
||||
if let Some(operation) = self.control.pending.as_mut() {
|
||||
operation.result = value;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn control_track(&mut self, task: Task<Message>) -> Task<Message> {
|
||||
if task.units() == 0 {
|
||||
return task;
|
||||
@@ -1101,10 +1112,12 @@ mod tests {
|
||||
);
|
||||
let started = request(&mut app, json!({"op":"start","cmd":"LINE"}));
|
||||
assert_eq!(started["status"], "waiting_input");
|
||||
assert!(started["state"]["command"]["accepts"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("point")));
|
||||
assert!(
|
||||
started["state"]["command"]["accepts"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("point"))
|
||||
);
|
||||
assert_eq!(
|
||||
started["state"]["command"]["input_example"]["kind"],
|
||||
"point"
|
||||
@@ -1129,10 +1142,12 @@ mod tests {
|
||||
);
|
||||
assert_eq!(app.automation_op(r#"{"op":"entities"}"#)["total"], 1);
|
||||
request(&mut app, json!({"op":"select","type":"LINE"}));
|
||||
assert!(!app.control_properties()["sections"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
assert!(
|
||||
!app.control_properties()["sections"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
let changed = request(&mut app, json!({"op":"property","field":"color","value":1}));
|
||||
assert_eq!(changed["ok"], true, "{changed}");
|
||||
assert_eq!(
|
||||
|
||||
+67
-58
@@ -9,21 +9,22 @@ pub(crate) mod config;
|
||||
pub use automation::{export_headless, serve};
|
||||
mod command_driver;
|
||||
pub(crate) mod commands;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod doc_api;
|
||||
mod document;
|
||||
pub(crate) mod expr_eval;
|
||||
mod find_replace;
|
||||
pub(crate) mod helpers;
|
||||
mod history;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
mod doc_api;
|
||||
mod layers;
|
||||
mod model_ops;
|
||||
mod mtext_editor;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod plugin_host;
|
||||
mod properties;
|
||||
mod presspull_ops;
|
||||
mod properties;
|
||||
mod recent;
|
||||
mod record_api;
|
||||
pub(crate) mod settings;
|
||||
mod shortcuts;
|
||||
mod startup;
|
||||
@@ -225,7 +226,10 @@ impl From<&QSelectState> for QSelectSettings {
|
||||
Self {
|
||||
scope: state.scope,
|
||||
type_filter: state.type_filter.clone(),
|
||||
property_field: state.property.as_ref().map(|property| property.field.clone()),
|
||||
property_field: state
|
||||
.property
|
||||
.as_ref()
|
||||
.map(|property| property.field.clone()),
|
||||
operator: state.operator,
|
||||
value: state.value.clone(),
|
||||
mode: state.mode,
|
||||
@@ -256,12 +260,12 @@ pub(crate) enum FindMatchKey {
|
||||
}
|
||||
use crate::snap::Snapper;
|
||||
use crate::ui::{CommandLine, Ribbon, StatusBar};
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
use acadrust::CadDocument;
|
||||
use acadrust::types::{Color as AcadColor, LineWeight};
|
||||
|
||||
use iced::time::Instant;
|
||||
use iced::window;
|
||||
use iced::{mouse, Point, Task, Theme};
|
||||
use iced::{Point, Task, Theme, mouse};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(super) const POLY_START_DELAY_MS: u128 = 150;
|
||||
@@ -318,9 +322,7 @@ struct AddSelectedRestore {
|
||||
}
|
||||
|
||||
/// Which Start-page section a narrow (tabbed) Start page is showing.
|
||||
#[derive(
|
||||
Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub enum StartSection {
|
||||
Recent,
|
||||
Videos,
|
||||
@@ -780,15 +782,13 @@ pub(super) struct OpenCADStudio {
|
||||
/// Live filter for installed and available plugin cards.
|
||||
plugin_search_input: String,
|
||||
/// Installable release tags fetched per linked repo (for the dropdown).
|
||||
repo_release_tags:
|
||||
rustc_hash::FxHashMap<String, Vec<crate::plugin::external::ReleaseInfo>>,
|
||||
repo_release_tags: rustc_hash::FxHashMap<String, Vec<crate::plugin::external::ReleaseInfo>>,
|
||||
/// The release tag currently selected per linked repo.
|
||||
repo_selected_tag: rustc_hash::FxHashMap<String, String>,
|
||||
/// Repository currently shown in the Plugin Manager detail pane.
|
||||
selected_plugin_repo: Option<String>,
|
||||
/// Parsed GitHub README content or the last fetch error, cached per repo.
|
||||
plugin_readmes:
|
||||
rustc_hash::FxHashMap<String, Result<iced::widget::markdown::Content, String>>,
|
||||
plugin_readmes: rustc_hash::FxHashMap<String, Result<iced::widget::markdown::Content, String>>,
|
||||
/// README requests in flight, used to render a deterministic loading state.
|
||||
plugin_readme_loading: rustc_hash::FxHashSet<String>,
|
||||
/// Last marketplace status / error line shown in the Plugin Manager.
|
||||
@@ -1098,8 +1098,7 @@ pub(super) struct OpenCADStudio {
|
||||
save_job_serial: u64,
|
||||
/// Destination leases held while Save As workers are active.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pending_save_leases:
|
||||
std::collections::HashMap<u64, crate::io::edit_lock::EditLease>,
|
||||
pending_save_leases: std::collections::HashMap<u64, crate::io::edit_lock::EditLease>,
|
||||
/// Locked-file failure currently shown in the recovery dialog.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pending_save_failure: Option<PendingSaveFailure>,
|
||||
@@ -1448,7 +1447,8 @@ impl ClipboardDeps {
|
||||
if !objects.is_empty() {
|
||||
let mut annotation_scales = Vec::new();
|
||||
for (_, object) in &objects {
|
||||
let acadrust::objects::ObjectType::ObjectContextData(context) = object else {
|
||||
let acadrust::objects::ObjectType::ObjectContextData(context) = object
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
if annotation_scales
|
||||
@@ -1618,9 +1618,7 @@ impl ClipboardDeps {
|
||||
fn snapshot_block(doc: &acadrust::CadDocument, name: &str) -> Option<BlockDef> {
|
||||
use acadrust::EntityType;
|
||||
let br = doc.block_records.get(name)?;
|
||||
if name.starts_with("*Model_Space")
|
||||
|| name.starts_with("*Paper_Space")
|
||||
|| br.flags.is_xref
|
||||
if name.starts_with("*Model_Space") || name.starts_with("*Paper_Space") || br.flags.is_xref
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -1835,10 +1833,7 @@ pub enum Message {
|
||||
/// Register a font already held by the shared web store with the UI renderer.
|
||||
ApplyWebFont(crate::scene::text::web_font::Script),
|
||||
/// Completion of the UI renderer's runtime font registration.
|
||||
WebUiFontLoaded(
|
||||
crate::scene::text::web_font::Script,
|
||||
Result<(), String>,
|
||||
),
|
||||
WebUiFontLoaded(crate::scene::text::web_font::Script, Result<(), String>),
|
||||
/// Ctrl+V. Routed by `update` into an open text editor, the drawing-object
|
||||
/// clipboard, or the system text clipboard.
|
||||
PasteShortcut,
|
||||
@@ -1906,10 +1901,13 @@ pub enum Message {
|
||||
WebFileOpened(u64, crate::io::WebOpenOutcome),
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
WebFileCached(u64, crate::io::WebOpenOutcome, Result<(), String>),
|
||||
FileOpened(u64, Result<
|
||||
(String, PathBuf, CadDocument, crate::scene::DerivedCaches),
|
||||
crate::io::OpenLoadError,
|
||||
>),
|
||||
FileOpened(
|
||||
u64,
|
||||
Result<
|
||||
(String, PathBuf, CadDocument, crate::scene::DerivedCaches),
|
||||
crate::io::OpenLoadError,
|
||||
>,
|
||||
),
|
||||
RecoveryClose,
|
||||
RecoveryAttempt,
|
||||
RecoveryDecline,
|
||||
@@ -2150,9 +2148,16 @@ pub enum Message {
|
||||
},
|
||||
/// A widget captured Up/Down; resolve it only if the command input owns
|
||||
/// keyboard focus.
|
||||
CommandLineArrowProbe { direction: ArrowKey, extend_selection: bool },
|
||||
CommandLineArrowProbe {
|
||||
direction: ArrowKey,
|
||||
extend_selection: bool,
|
||||
},
|
||||
/// Result of the command-input focus query for a captured Up/Down key.
|
||||
CommandLineArrowResolved { direction: ArrowKey, focused: bool, extend_selection: bool },
|
||||
CommandLineArrowResolved {
|
||||
direction: ArrowKey,
|
||||
focused: bool,
|
||||
extend_selection: bool,
|
||||
},
|
||||
/// Toggle the dropdown listing the full command-line history.
|
||||
CommandHistoryToggle,
|
||||
/// Grab/move/release the expanded history panel's top resize edge.
|
||||
@@ -2303,9 +2308,7 @@ pub enum Message {
|
||||
epoch: u64,
|
||||
source: usize,
|
||||
wires: std::sync::Weak<Vec<crate::scene::WireModel>>,
|
||||
index: std::sync::Arc<
|
||||
crate::scene::pick::interaction_index::InteractionIndex,
|
||||
>,
|
||||
index: std::sync::Arc<crate::scene::pick::interaction_index::InteractionIndex>,
|
||||
build_ms: f64,
|
||||
},
|
||||
WindowResized(f32, f32),
|
||||
@@ -2559,7 +2562,10 @@ pub enum Message {
|
||||
PropEditChoiceToggle,
|
||||
/// User is typing in a block-attribute value field (live buffer update),
|
||||
/// keyed by the attribute tag.
|
||||
PropAttrInput { tag: String, value: String },
|
||||
PropAttrInput {
|
||||
tag: String,
|
||||
value: String,
|
||||
},
|
||||
/// User committed a block-attribute value edit (Enter pressed).
|
||||
PropAttrCommit(String),
|
||||
/// Reports the currently keyboard-focused widget (if any) after a
|
||||
@@ -2585,7 +2591,10 @@ pub enum Message {
|
||||
PropColorFieldToggle(String),
|
||||
/// User picked a colour for a generic per-field colour row (hatch gradient
|
||||
/// `Color 1` / `Color 2`), routed by the field name.
|
||||
PropColorFieldChanged { field: String, color: AcadColor },
|
||||
PropColorFieldChanged {
|
||||
field: String,
|
||||
color: AcadColor,
|
||||
},
|
||||
/// Collapse the inline color picker dropdown. Fired when another
|
||||
/// properties-panel dropdown (a combo_box) opens, so at most one panel
|
||||
/// dropdown is open at a time and they can't overlap. (#235)
|
||||
@@ -2697,7 +2706,10 @@ pub enum Message {
|
||||
/// Select the row the Text Options / Properties tabs act on.
|
||||
AttrEditorSelect(usize),
|
||||
/// Live edit of the attribute value at row `idx` (Attribute tab).
|
||||
AttrEditorInput { idx: usize, value: String },
|
||||
AttrEditorInput {
|
||||
idx: usize,
|
||||
value: String,
|
||||
},
|
||||
// Text Options — all act on the selected row:
|
||||
AttrEditorTextStyle(String),
|
||||
AttrEditorJustify(String),
|
||||
@@ -2752,9 +2764,7 @@ pub enum Message {
|
||||
/// GitHub Discussions fetched at boot for the Start page.
|
||||
DiscussionsFetched(Result<Vec<crate::discussions::DiscussionEntry>, String>),
|
||||
/// Recent-file DWG preview thumbnails decoded on a background thread.
|
||||
RecentThumbsLoaded(
|
||||
Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>,
|
||||
),
|
||||
RecentThumbsLoaded(Vec<(std::path::PathBuf, Option<iced::widget::image::Handle>)>),
|
||||
/// Installable releases and manifest API versions fetched for `owner/repo`.
|
||||
PluginReleasesFetched(
|
||||
String,
|
||||
@@ -2795,7 +2805,10 @@ pub enum Message {
|
||||
InvertSelection,
|
||||
/// Keyboard modifier state changed — tracks whether Shift is held so the
|
||||
/// pick path can do subtractive (Shift+click) selection.
|
||||
SetModifiers { shift: bool, ctrl: bool },
|
||||
SetModifiers {
|
||||
shift: bool,
|
||||
ctrl: bool,
|
||||
},
|
||||
// ── In-place MText editor ───────────────────────────────────────────
|
||||
/// Text-area edit action from the multi-line editor widget.
|
||||
MTextEdit(iced::widget::text_editor::Action),
|
||||
@@ -3265,10 +3278,7 @@ impl OpenCADStudio {
|
||||
.iter()
|
||||
.filter_map(|v| {
|
||||
let bytes = v.thumb.clone()?;
|
||||
Some((
|
||||
v.id.clone(),
|
||||
iced::widget::image::Handle::from_bytes(bytes),
|
||||
))
|
||||
Some((v.id.clone(), iced::widget::image::Handle::from_bytes(bytes)))
|
||||
})
|
||||
.collect();
|
||||
self.videos = videos;
|
||||
@@ -3782,8 +3792,7 @@ impl OpenCADStudio {
|
||||
let state = Self::new();
|
||||
let (id, open_task) = window::open(window::Settings {
|
||||
maximized: true,
|
||||
icon: build_window_icon()
|
||||
.and_then(|rgba| window::icon::from_rgba(rgba, 32, 32).ok()),
|
||||
icon: build_window_icon().and_then(|rgba| window::icon::from_rgba(rgba, 32, 32).ok()),
|
||||
exit_on_close_request: false,
|
||||
// A Wayland compositor has no StartupWMClass to go on: it resolves a
|
||||
// window's dock icon by matching the window's app_id against the
|
||||
@@ -3919,24 +3928,17 @@ impl OpenCADStudio {
|
||||
let mut s = Self::new();
|
||||
s.queue_startup_prompts();
|
||||
let focus = s.focus_cmd_input();
|
||||
let primary_font = crate::scene::text::web_font::preload_language(
|
||||
&crate::i18n::active_language_tag(),
|
||||
);
|
||||
let primary_font =
|
||||
crate::scene::text::web_font::preload_language(&crate::i18n::active_language_tag());
|
||||
let fonts = Task::batch([
|
||||
Task::done(Message::PollWebFonts),
|
||||
Task::done(Message::ApplyWebFont(primary_font)),
|
||||
]);
|
||||
// Web can't reach the Patreon API directly (CORS); fetch the CI-built
|
||||
// supporters.json served on the same origin instead.
|
||||
let patrons = Task::perform(
|
||||
crate::patreon::fetch_patrons_web(),
|
||||
Message::PatronsFetched,
|
||||
);
|
||||
let patrons = Task::perform(crate::patreon::fetch_patrons_web(), Message::PatronsFetched);
|
||||
s.videos_loading = true;
|
||||
let videos = Task::perform(
|
||||
crate::videos::fetch_playlist_web(),
|
||||
Message::VideosFetched,
|
||||
);
|
||||
let videos = Task::perform(crate::videos::fetch_playlist_web(), Message::VideosFetched);
|
||||
s.discussions_loading = true;
|
||||
let discussions = Task::perform(
|
||||
crate::discussions::fetch_discussions_web(),
|
||||
@@ -3969,7 +3971,12 @@ pub fn run() -> iced::Result {
|
||||
if let Some(tab) = state.tabs.get(state.active_tab) {
|
||||
let dot = if tab.dirty { "● " } else { "" };
|
||||
let name = tab.tab_display_name();
|
||||
format!("{}Open CAD Studio {} - {}", dot, env!("OCS_APP_VERSION"), name)
|
||||
format!(
|
||||
"{}Open CAD Studio {} - {}",
|
||||
dot,
|
||||
env!("OCS_APP_VERSION"),
|
||||
name
|
||||
)
|
||||
} else {
|
||||
concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string()
|
||||
}
|
||||
@@ -4003,7 +4010,9 @@ pub fn run_web() -> iced::Result {
|
||||
OpenCADStudio::view_main,
|
||||
)
|
||||
.subscription(OpenCADStudio::subscription)
|
||||
.title(|_state: &OpenCADStudio| concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string())
|
||||
.title(|_state: &OpenCADStudio| {
|
||||
concat!("Open CAD Studio ", env!("OCS_APP_VERSION")).to_string()
|
||||
})
|
||||
.theme(|state: &OpenCADStudio| state.active_theme.clone())
|
||||
.backend(iced::Backend::Hardware(iced::backend::Api::OpenGL))
|
||||
.font(iced_aw::ICED_AW_FONT_BYTES)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
+9
-8
@@ -1,25 +1,26 @@
|
||||
#![allow(non_snake_case)]
|
||||
#![recursion_limit = "256"]
|
||||
|
||||
pub mod app;
|
||||
pub mod config;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod cli;
|
||||
pub mod command;
|
||||
pub mod config;
|
||||
pub mod discussions;
|
||||
pub mod entities;
|
||||
pub mod i18n;
|
||||
pub mod io;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod network;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub mod mcp;
|
||||
pub mod modules;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod network;
|
||||
pub mod par;
|
||||
pub mod patreon;
|
||||
pub mod discussions;
|
||||
pub mod videos;
|
||||
pub mod plugin;
|
||||
pub mod perf;
|
||||
pub mod plugin;
|
||||
pub mod scene;
|
||||
pub mod snap;
|
||||
pub mod ui;
|
||||
pub mod par;
|
||||
pub mod sys;
|
||||
pub mod ui;
|
||||
pub mod videos;
|
||||
+86
-14
@@ -22,11 +22,13 @@ const MODERN_PROTOCOL_VERSION: &str = "2026-07-28";
|
||||
const MAX_REQUEST: usize = 1_048_576;
|
||||
const MAX_RESPONSE: u64 = 16 * 1024 * 1024;
|
||||
const CACHE_TTL_MS: u64 = 3_600_000;
|
||||
const INSTRUCTIONS: &str = "Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_execute and ocs_capture. Preserve document_id, revision, selection and request_id. Use ocs_read commands with parameters.name for a command manifest. Use ocs_execute batch when several steps are known, and request changed_entities when the resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path.";
|
||||
const INSTRUCTIONS: &str = "Call ocs_sessions, then pass its session_id as ocs_session_id to ocs_read, ocs_execute and ocs_capture. Read capabilities to discover the complete CAD automation surface. Use records to inspect every serializable entity, object, table, header and document record; filter with RFC 6901 JSON Pointer paths. Use set_properties for atomic, type-checked record edits and preserve document_id, revision and request_id. Use commands with parameters.name for a command manifest. Use batch when several steps are known, and request changed_entities when resulting geometry is needed. For interactive work, call start and follow state.command.accepts, options and input_example. A run.cmd contains the command name followed by prompt answers separated by spaces; points use x,y or x,y,z. After a timeout, query the existing operation and never replay a mutation with a new request_id. waiting_input and running are not completion. Let OCS and its geometry kernel calculate geometry; use query near, contains_point and intersections for exact relationships. Verify important results with queries and a viewport capture, and save only to an explicit path.";
|
||||
const READ_OPS: &[&str] = &[
|
||||
"state",
|
||||
"hello",
|
||||
"query",
|
||||
"records",
|
||||
"capabilities",
|
||||
"entities",
|
||||
"layers",
|
||||
"header",
|
||||
@@ -38,12 +40,39 @@ const READ_OPS: &[&str] = &[
|
||||
"operation",
|
||||
];
|
||||
const EXECUTE_OPS: &[&str] = &[
|
||||
"new", "open", "activate", "run", "start", "input", "cancel", "undo", "redo", "select",
|
||||
"property", "action", "save", "stop", "batch",
|
||||
"new",
|
||||
"open",
|
||||
"activate",
|
||||
"run",
|
||||
"start",
|
||||
"input",
|
||||
"cancel",
|
||||
"undo",
|
||||
"redo",
|
||||
"select",
|
||||
"property",
|
||||
"set_properties",
|
||||
"action",
|
||||
"save",
|
||||
"stop",
|
||||
"batch",
|
||||
];
|
||||
const BATCH_STEP_OPS: &[&str] = &[
|
||||
"new", "open", "activate", "run", "start", "input", "cancel", "undo", "redo", "select",
|
||||
"property", "action", "save", "stop",
|
||||
"new",
|
||||
"open",
|
||||
"activate",
|
||||
"run",
|
||||
"start",
|
||||
"input",
|
||||
"cancel",
|
||||
"undo",
|
||||
"redo",
|
||||
"select",
|
||||
"property",
|
||||
"set_properties",
|
||||
"action",
|
||||
"save",
|
||||
"stop",
|
||||
];
|
||||
const MAX_BATCH_STEPS: usize = 64;
|
||||
|
||||
@@ -671,6 +700,15 @@ fn validate_execute_request(request: &Value, op: &str) -> Result<(), String> {
|
||||
r#"{"op":"property","field":"color","value":1}"#,
|
||||
)
|
||||
}
|
||||
"set_properties"
|
||||
if request["collection"].as_str().is_none_or(str::is_empty)
|
||||
|| request["updates"].as_array().is_none_or(Vec::is_empty) =>
|
||||
{
|
||||
missing(
|
||||
"collection or updates",
|
||||
r#"{"op":"set_properties","collection":"entities","handle":"2A","updates":[{"path":"/common/layer","value":"Walls"}]}"#,
|
||||
)
|
||||
}
|
||||
"action" => match request["name"].as_str() {
|
||||
Some(name) if crate::app::automation_action_names().contains(&name) => Ok(()),
|
||||
Some(name) => Err(format!(
|
||||
@@ -833,6 +871,8 @@ fn batch_step_schema() -> Value {
|
||||
"clear":{"type":"boolean"},
|
||||
"field":{"type":"string"},
|
||||
"value":{"description":"New property value; its JSON type must match the property kind.","anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object"},{"type":"array"},{"type":"null"}]},
|
||||
"collection":{"type":"string"},
|
||||
"updates":{"type":"array","minItems":1,"items":{"type":"object","properties":{"path":{"type":"string","pattern":"^/"},"value":{},"expected":{}},"required":["path","value"],"additionalProperties":false}},
|
||||
"name":{"type":"string","enum":crate::app::automation_action_names()}
|
||||
},
|
||||
"required":["op"],
|
||||
@@ -873,6 +913,8 @@ fn execute_request_schema() -> Value {
|
||||
"clear":{"type":"boolean","description":"Clear the current selection before applying select filters."},
|
||||
"field":{"type":"string","minLength":1,"description":"Property id returned by ocs_read properties."},
|
||||
"value":{"description":"New property value; its JSON type must match the property kind.","anyOf":[{"type":"string"},{"type":"number"},{"type":"boolean"},{"type":"object"},{"type":"array"},{"type":"null"}]},
|
||||
"collection":{"type":"string","description":"Record collection returned by ocs_read records."},
|
||||
"updates":{"type":"array","minItems":1,"description":"Atomic, type-checked property replacements. Paths are RFC 6901 JSON Pointers relative to record.properties.","items":{"type":"object","properties":{"path":{"type":"string","pattern":"^/"},"value":{},"expected":{"description":"Optional compare-and-set value."}},"required":["path","value"],"additionalProperties":false}},
|
||||
"name":{"type":"string","enum":crate::app::automation_action_names(),"description":"UI action returned by ocs_read commands."},
|
||||
"steps":{"type":"array","minItems":1,"maxItems":MAX_BATCH_STEPS,"description":"Sequential editor operations executed with fresh state and idempotency keys. Execution stops at the first failure; completed_steps says what committed.","items":batch_step_schema()}
|
||||
},
|
||||
@@ -898,6 +940,7 @@ fn execute_request_schema() -> Value {
|
||||
{"properties":{"op":{"const":"redo"}}},
|
||||
{"properties":{"op":{"const":"select"}}},
|
||||
{"properties":{"op":{"const":"property"}},"required":["field","value"]},
|
||||
{"properties":{"op":{"const":"set_properties"}},"required":["collection","updates"]},
|
||||
{"properties":{"op":{"const":"action"}},"required":["name"]},
|
||||
{"properties":{"op":{"const":"save"}}},
|
||||
{"properties":{"op":{"const":"stop"}}},
|
||||
@@ -943,8 +986,8 @@ fn tool_definitions() -> Value {
|
||||
},
|
||||
{
|
||||
"name":"ocs_read",
|
||||
"description":"Read state, command manifests, entities, layers, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
||||
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name for detailed commands help."},"search":{"type":"string","description":"Case-insensitive command-name search."},"document_id":{"type":"integer","minimum":0},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity handles for query or measure."},"type":{"type":"string","description":"Entity type filter for query."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
||||
"description":"Discover capabilities or read state, complete database records, command manifests, entities, properties, kernel measurements and spatial relationships, history, events or operation status from a live OCS session.",
|
||||
"inputSchema":{"type":"object","properties":{"ocs_session_id":{"type":"string","minLength":1,"description":"Value of session_id returned by ocs_sessions."},"op":{"type":"string","enum":READ_OPS,"default":"state"},"parameters":{"type":"object","description":"Operation-specific filters.","properties":{"name":{"type":"string","description":"Command name or record name."},"search":{"type":"string","description":"Case-insensitive command-name search."},"document_id":{"type":"integer","minimum":0},"collection":{"type":"string","description":"Record collection, all, or omit to discover collections."},"handle":{"type":"string"},"handles":{"type":"array","items":{"type":"string"},"description":"Exact entity or record handles."},"type":{"type":"string","description":"Entity or record type filter."},"layer":{"type":"string","description":"Layer name filter for query."},"detail":{"type":"string","enum":["summary","geometry","full"],"default":"geometry","description":"Entity detail returned by query."},"fields":{"type":"array","items":{"type":"string"},"description":"Return only these entity fields plus handle."},"paths":{"type":"array","items":{"type":"string"},"description":"Project RFC 6901 JSON Pointer paths relative to record.properties."},"where":{"type":"array","description":"All property filters must match.","items":{"type":"object","properties":{"path":{"type":"string"},"op":{"type":"string","enum":["eq","ne","lt","lte","gt","gte","contains","starts_with","ends_with","in","exists","not_exists"],"default":"eq"},"value":{}},"required":["path"],"additionalProperties":false}},"near":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Rank planar curves by exact kernel distance to this world XY point."},"contains_point":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":3,"description":"Return closed planar curves containing this world XY point."},"bounds":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4,"description":"Filter entities whose world XY bounds overlap [min_x,min_y,max_x,max_y]."},"intersections":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":2,"description":"Return exact kernel intersections between two planar curve handles."},"after":{"type":"integer","minimum":0,"description":"Event cursor."},"request_id":{"type":"string","description":"Operation id to query."},"offset":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":1,"maximum":10000}},"additionalProperties":false}},"required":["ocs_session_id"],"additionalProperties":false},
|
||||
"outputSchema":read_output_schema(),
|
||||
"annotations":{"title":"Read OCS state","readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false}
|
||||
},
|
||||
@@ -1272,13 +1315,17 @@ mod tests {
|
||||
);
|
||||
assert_eq!(tools[0]["annotations"]["readOnlyHint"], false);
|
||||
for tool in [&tools[1], &tools[2], &tools[3]] {
|
||||
assert!(tool["inputSchema"]["properties"]
|
||||
.get("session_id")
|
||||
.is_none());
|
||||
assert!(tool["inputSchema"]["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("ocs_session_id")));
|
||||
assert!(
|
||||
tool["inputSchema"]["properties"]
|
||||
.get("session_id")
|
||||
.is_none()
|
||||
);
|
||||
assert!(
|
||||
tool["inputSchema"]["required"]
|
||||
.as_array()
|
||||
.unwrap()
|
||||
.contains(&json!("ocs_session_id"))
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
tools[2]["inputSchema"]["properties"]["request"]["required"],
|
||||
@@ -1307,6 +1354,31 @@ mod tests {
|
||||
tools[2]["inputSchema"]["properties"]["request"]["properties"]["cmd"]["examples"][0],
|
||||
"LINE 0,0 10,10"
|
||||
);
|
||||
assert!(READ_OPS.contains(&"capabilities"));
|
||||
assert!(READ_OPS.contains(&"records"));
|
||||
assert!(EXECUTE_OPS.contains(&"set_properties"));
|
||||
assert_eq!(
|
||||
tools[1]["inputSchema"]["properties"]["parameters"]["properties"]["where"]["items"]["properties"]
|
||||
["op"]["enum"],
|
||||
json!([
|
||||
"eq",
|
||||
"ne",
|
||||
"lt",
|
||||
"lte",
|
||||
"gt",
|
||||
"gte",
|
||||
"contains",
|
||||
"starts_with",
|
||||
"ends_with",
|
||||
"in",
|
||||
"exists",
|
||||
"not_exists"
|
||||
])
|
||||
);
|
||||
assert_eq!(
|
||||
tools[2]["inputSchema"]["properties"]["request"]["properties"]["updates"]["items"]["required"],
|
||||
json!(["path", "value"])
|
||||
);
|
||||
assert!(tools[0].get("outputSchema").is_some());
|
||||
assert!(tools[1].get("outputSchema").is_some());
|
||||
assert!(tools[2].get("outputSchema").is_some());
|
||||
|
||||
Reference in new issue
Block a user