feat: add item-id queries and geometry inspection APIs
This commit is contained in:
parent
08c2b6f919
commit
1b54e688c1
|
|
@ -101,6 +101,30 @@ Show pad-level netlist entries (footprint/pad/net):
|
||||||
cargo run --bin kicad-ipc-cli -- netlist-pads
|
cargo run --bin kicad-ipc-cli -- netlist-pads
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Show parsed details for specific item IDs:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- items-by-id --id <uuid> --id <uuid>
|
||||||
|
```
|
||||||
|
|
||||||
|
Show item bounding boxes:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- item-bbox --id <uuid>
|
||||||
|
```
|
||||||
|
|
||||||
|
Include child text in the bounding box (for items such as footprints):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- item-bbox --id <uuid> --include-text
|
||||||
|
```
|
||||||
|
|
||||||
|
Run hit-test on a specific item:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo run --bin kicad-ipc-cli -- hit-test --id <uuid> --x-nm <x> --y-nm <y> --tolerance-nm 0
|
||||||
|
```
|
||||||
|
|
||||||
Get current project path (derived from open PCB docs):
|
Get current project path (derived from open PCB docs):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
287
src/client.rs
287
src/client.rs
|
|
@ -9,8 +9,8 @@ use crate::model::board::{
|
||||||
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
||||||
};
|
};
|
||||||
use crate::model::common::{
|
use crate::model::common::{
|
||||||
DocumentSpecifier, DocumentType, ProjectInfo, SelectionItemDetail, SelectionSummary,
|
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, ProjectInfo,
|
||||||
SelectionTypeCount, VersionInfo,
|
SelectionItemDetail, SelectionSummary, SelectionTypeCount, VersionInfo,
|
||||||
};
|
};
|
||||||
use crate::proto::kiapi::board::commands as board_commands;
|
use crate::proto::kiapi::board::commands as board_commands;
|
||||||
use crate::proto::kiapi::board::types as board_types;
|
use crate::proto::kiapi::board::types as board_types;
|
||||||
|
|
@ -31,6 +31,9 @@ const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers";
|
||||||
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
||||||
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
|
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
|
||||||
const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
|
const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
|
||||||
|
const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById";
|
||||||
|
const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox";
|
||||||
|
const CMD_HIT_TEST: &str = "kiapi.common.commands.HitTest";
|
||||||
|
|
||||||
const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
|
const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
|
||||||
const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
|
const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
|
||||||
|
|
@ -41,6 +44,8 @@ const RES_BOARD_LAYERS: &str = "kiapi.board.commands.BoardLayers";
|
||||||
const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
|
const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
|
||||||
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
||||||
const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
|
const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
|
||||||
|
const RES_GET_BOUNDING_BOX_RESPONSE: &str = "kiapi.common.commands.GetBoundingBoxResponse";
|
||||||
|
const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse";
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct KiCadClient {
|
pub struct KiCadClient {
|
||||||
|
|
@ -318,13 +323,8 @@ impl KiCadClient {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
|
pub async fn get_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
let document = self.current_board_document_proto().await?;
|
|
||||||
let command = common_commands::GetSelection {
|
let command = common_commands::GetSelection {
|
||||||
header: Some(common_types::ItemHeader {
|
header: Some(self.current_board_item_header().await?),
|
||||||
document: Some(document),
|
|
||||||
container: None,
|
|
||||||
field_mask: None,
|
|
||||||
}),
|
|
||||||
types: Vec::new(),
|
types: Vec::new(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -340,19 +340,7 @@ impl KiCadClient {
|
||||||
|
|
||||||
pub async fn get_selection_details(&self) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
pub async fn get_selection_details(&self) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
let items = self.get_selection_raw().await?;
|
let items = self.get_selection_raw().await?;
|
||||||
let mut details = Vec::with_capacity(items.len());
|
summarize_item_details(items)
|
||||||
for item in items {
|
|
||||||
let raw_len = item.value.len();
|
|
||||||
let type_url = item.type_url.clone();
|
|
||||||
let detail = selection_item_detail(&item)?;
|
|
||||||
details.push(SelectionItemDetail {
|
|
||||||
type_url,
|
|
||||||
detail,
|
|
||||||
raw_len,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(details)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError> {
|
pub async fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError> {
|
||||||
|
|
@ -362,6 +350,101 @@ impl KiCadClient {
|
||||||
pad_netlist_from_footprint_items(footprint_items)
|
pad_netlist_from_footprint_items(footprint_items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_items_by_id_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = common_commands::GetItemsById {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| common_types::Kiid { value: id })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_ID))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetItemsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
|
||||||
|
ensure_item_request_ok(payload.status)?;
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_items_by_id_details(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
|
let items = self.get_items_by_id_raw(item_ids).await?;
|
||||||
|
summarize_item_details(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_item_bounding_boxes(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
include_child_text: bool,
|
||||||
|
) -> Result<Vec<ItemBoundingBox>, KiCadError> {
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = if include_child_text {
|
||||||
|
common_commands::BoundingBoxMode::BbmItemAndChildText
|
||||||
|
} else {
|
||||||
|
common_commands::BoundingBoxMode::BbmItemOnly
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = common_commands::GetBoundingBox {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| common_types::Kiid { value: id })
|
||||||
|
.collect(),
|
||||||
|
mode: mode as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_BOUNDING_BOX))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetBoundingBoxResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_BOUNDING_BOX_RESPONSE)?;
|
||||||
|
|
||||||
|
map_item_bounding_boxes(payload.items, payload.boxes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn hit_test_item(
|
||||||
|
&self,
|
||||||
|
item_id: String,
|
||||||
|
position: Vector2Nm,
|
||||||
|
tolerance_nm: i32,
|
||||||
|
) -> Result<ItemHitTestResult, KiCadError> {
|
||||||
|
let command = common_commands::HitTest {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
id: Some(common_types::Kiid { value: item_id }),
|
||||||
|
position: Some(common_types::Vector2 {
|
||||||
|
x_nm: position.x_nm,
|
||||||
|
y_nm: position.y_nm,
|
||||||
|
}),
|
||||||
|
tolerance: tolerance_nm,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_HIT_TEST))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::HitTestResponse =
|
||||||
|
envelope::unpack_any(&response, RES_HIT_TEST_RESPONSE)?;
|
||||||
|
|
||||||
|
Ok(map_hit_test_result(payload.result))
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_command(
|
async fn send_command(
|
||||||
&self,
|
&self,
|
||||||
command: prost_types::Any,
|
command: prost_types::Any,
|
||||||
|
|
@ -406,14 +489,17 @@ impl KiCadClient {
|
||||||
Ok(model_document_to_proto(selected))
|
Ok(model_document_to_proto(selected))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn current_board_item_header(&self) -> Result<common_types::ItemHeader, KiCadError> {
|
||||||
|
Ok(common_types::ItemHeader {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
container: None,
|
||||||
|
field_mask: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_items_raw(&self, types: Vec<i32>) -> Result<Vec<prost_types::Any>, KiCadError> {
|
async fn get_items_raw(&self, types: Vec<i32>) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
let document = self.current_board_document_proto().await?;
|
|
||||||
let command = common_commands::GetItems {
|
let command = common_commands::GetItems {
|
||||||
header: Some(common_types::ItemHeader {
|
header: Some(self.current_board_item_header().await?),
|
||||||
document: Some(document),
|
|
||||||
container: None,
|
|
||||||
field_mask: None,
|
|
||||||
}),
|
|
||||||
types,
|
types,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -424,15 +510,7 @@ impl KiCadClient {
|
||||||
let payload: common_commands::GetItemsResponse =
|
let payload: common_commands::GetItemsResponse =
|
||||||
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
|
||||||
let request_status = common_types::ItemRequestStatus::try_from(payload.status)
|
ensure_item_request_ok(payload.status)?;
|
||||||
.unwrap_or(common_types::ItemRequestStatus::IrsUnknown);
|
|
||||||
|
|
||||||
if request_status != common_types::ItemRequestStatus::IrsOk {
|
|
||||||
return Err(KiCadError::ItemStatus {
|
|
||||||
code: request_status.as_str_name().to_string(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(payload.items)
|
Ok(payload.items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -522,6 +600,73 @@ fn summarize_selection(items: Vec<prost_types::Any>) -> SelectionSummary {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn summarize_item_details(
|
||||||
|
items: Vec<prost_types::Any>,
|
||||||
|
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
|
let mut details = Vec::with_capacity(items.len());
|
||||||
|
for item in items {
|
||||||
|
let raw_len = item.value.len();
|
||||||
|
let type_url = item.type_url.clone();
|
||||||
|
let detail = selection_item_detail(&item)?;
|
||||||
|
details.push(SelectionItemDetail {
|
||||||
|
type_url,
|
||||||
|
detail,
|
||||||
|
raw_len,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(details)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_item_request_ok(status: i32) -> Result<(), KiCadError> {
|
||||||
|
let request_status = common_types::ItemRequestStatus::try_from(status)
|
||||||
|
.unwrap_or(common_types::ItemRequestStatus::IrsUnknown);
|
||||||
|
|
||||||
|
if request_status != common_types::ItemRequestStatus::IrsOk {
|
||||||
|
return Err(KiCadError::ItemStatus {
|
||||||
|
code: request_status.as_str_name().to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_item_bounding_boxes(
|
||||||
|
item_ids: Vec<common_types::Kiid>,
|
||||||
|
boxes: Vec<common_types::Box2>,
|
||||||
|
) -> Result<Vec<ItemBoundingBox>, KiCadError> {
|
||||||
|
let mut mapped = Vec::with_capacity(item_ids.len().min(boxes.len()));
|
||||||
|
for (item_id, bbox) in item_ids.into_iter().zip(boxes.into_iter()) {
|
||||||
|
let position = bbox.position.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: format!("missing bounding-box position for item `{}`", item_id.value),
|
||||||
|
})?;
|
||||||
|
let size = bbox.size.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: format!("missing bounding-box size for item `{}`", item_id.value),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
mapped.push(ItemBoundingBox {
|
||||||
|
item_id: item_id.value,
|
||||||
|
x_nm: position.x_nm,
|
||||||
|
y_nm: position.y_nm,
|
||||||
|
width_nm: size.x_nm,
|
||||||
|
height_nm: size.y_nm,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(mapped)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_hit_test_result(value: i32) -> ItemHitTestResult {
|
||||||
|
let result = common_commands::HitTestResult::try_from(value)
|
||||||
|
.unwrap_or(common_commands::HitTestResult::HtrUnknown);
|
||||||
|
|
||||||
|
match result {
|
||||||
|
common_commands::HitTestResult::HtrHit => ItemHitTestResult::Hit,
|
||||||
|
common_commands::HitTestResult::HtrNoHit => ItemHitTestResult::NoHit,
|
||||||
|
common_commands::HitTestResult::HtrUnknown => ItemHitTestResult::Unknown,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn decode_any<T: prost::Message + Default>(
|
fn decode_any<T: prost::Message + Default>(
|
||||||
payload: &prost_types::Any,
|
payload: &prost_types::Any,
|
||||||
expected_type_name: &str,
|
expected_type_name: &str,
|
||||||
|
|
@ -789,9 +934,10 @@ fn default_client_name() -> String {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
layer_to_model, model_document_to_proto, normalize_socket_uri,
|
ensure_item_request_ok, layer_to_model, map_hit_test_result, map_item_bounding_boxes,
|
||||||
pad_netlist_from_footprint_items, select_single_board_document, select_single_project_path,
|
model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items,
|
||||||
selection_item_detail, summarize_selection,
|
select_single_board_document, select_single_project_path, selection_item_detail,
|
||||||
|
summarize_item_details, summarize_selection,
|
||||||
};
|
};
|
||||||
use crate::error::KiCadError;
|
use crate::error::KiCadError;
|
||||||
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
||||||
|
|
@ -1072,4 +1218,67 @@ mod tests {
|
||||||
assert_eq!(entry.pad_number, "1");
|
assert_eq!(entry.pad_number, "1");
|
||||||
assert_eq!(entry.net_code, Some(5));
|
assert_eq!(entry.net_code, Some(5));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ensure_item_request_ok_accepts_ok_and_rejects_non_ok() {
|
||||||
|
assert!(ensure_item_request_ok(
|
||||||
|
crate::proto::kiapi::common::types::ItemRequestStatus::IrsOk as i32
|
||||||
|
)
|
||||||
|
.is_ok());
|
||||||
|
|
||||||
|
assert!(ensure_item_request_ok(
|
||||||
|
crate::proto::kiapi::common::types::ItemRequestStatus::IrsDocumentNotFound as i32
|
||||||
|
)
|
||||||
|
.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn summarize_item_details_reports_unknown_payload_as_unparsed() {
|
||||||
|
let items = vec![prost_types::Any {
|
||||||
|
type_url: "type.googleapis.com/kiapi.board.types.UnknownThing".to_string(),
|
||||||
|
value: vec![1, 2, 3, 4],
|
||||||
|
}];
|
||||||
|
|
||||||
|
let details =
|
||||||
|
summarize_item_details(items).expect("unknown types should still produce detail rows");
|
||||||
|
assert_eq!(details.len(), 1);
|
||||||
|
assert!(details[0].detail.contains("unparsed payload"));
|
||||||
|
assert_eq!(details[0].raw_len, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_item_bounding_boxes_maps_ids_and_dimensions() {
|
||||||
|
let ids = vec![crate::proto::kiapi::common::types::Kiid {
|
||||||
|
value: "id-1".to_string(),
|
||||||
|
}];
|
||||||
|
let boxes = vec![crate::proto::kiapi::common::types::Box2 {
|
||||||
|
position: Some(crate::proto::kiapi::common::types::Vector2 { x_nm: 10, y_nm: 20 }),
|
||||||
|
size: Some(crate::proto::kiapi::common::types::Vector2 { x_nm: 30, y_nm: 40 }),
|
||||||
|
}];
|
||||||
|
|
||||||
|
let mapped = map_item_bounding_boxes(ids, boxes)
|
||||||
|
.expect("box mapping should succeed when position and size are present");
|
||||||
|
assert_eq!(mapped.len(), 1);
|
||||||
|
assert_eq!(mapped[0].item_id, "id-1");
|
||||||
|
assert_eq!(mapped[0].x_nm, 10);
|
||||||
|
assert_eq!(mapped[0].y_nm, 20);
|
||||||
|
assert_eq!(mapped[0].width_nm, 30);
|
||||||
|
assert_eq!(mapped[0].height_nm, 40);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn map_hit_test_result_covers_known_variants() {
|
||||||
|
assert_eq!(
|
||||||
|
map_hit_test_result(
|
||||||
|
crate::proto::kiapi::common::commands::HitTestResult::HtrHit as i32
|
||||||
|
),
|
||||||
|
crate::model::common::ItemHitTestResult::Hit
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
map_hit_test_result(
|
||||||
|
crate::proto::kiapi::common::commands::HitTestResult::HtrNoHit as i32
|
||||||
|
),
|
||||||
|
crate::model::common::ItemHitTestResult::NoHit
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,9 @@ pub enum KiCadError {
|
||||||
#[error("item request status error `{code}`")]
|
#[error("item request status error `{code}`")]
|
||||||
ItemStatus { code: String },
|
ItemStatus { code: String },
|
||||||
|
|
||||||
|
#[error("invalid API response: {reason}")]
|
||||||
|
InvalidResponse { reason: String },
|
||||||
|
|
||||||
#[error("API response missing payload for `{expected_type_url}`")]
|
#[error("API response missing payload for `{expected_type_url}`")]
|
||||||
MissingPayload { expected_type_url: String },
|
MissingPayload { expected_type_url: String },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,6 @@ pub use crate::model::board::{
|
||||||
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
|
||||||
};
|
};
|
||||||
pub use crate::model::common::{
|
pub use crate::model::common::{
|
||||||
DocumentSpecifier, DocumentType, SelectionItemDetail, SelectionSummary, SelectionTypeCount,
|
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, SelectionItemDetail,
|
||||||
VersionInfo,
|
SelectionSummary, SelectionTypeCount, VersionInfo,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -111,3 +111,31 @@ pub struct SelectionItemDetail {
|
||||||
pub detail: String,
|
pub detail: String,
|
||||||
pub raw_len: usize,
|
pub raw_len: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct ItemBoundingBox {
|
||||||
|
pub item_id: String,
|
||||||
|
pub x_nm: i64,
|
||||||
|
pub y_nm: i64,
|
||||||
|
pub width_nm: i64,
|
||||||
|
pub height_nm: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub enum ItemHitTestResult {
|
||||||
|
Unknown,
|
||||||
|
NoHit,
|
||||||
|
Hit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ItemHitTestResult {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let value = match self {
|
||||||
|
Self::Unknown => "unknown",
|
||||||
|
Self::NoHit => "no-hit",
|
||||||
|
Self::Hit => "hit",
|
||||||
|
};
|
||||||
|
|
||||||
|
write!(f, "{value}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use std::process::ExitCode;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use kicad_ipc::{BoardOriginKind, ClientBuilder, DocumentType, KiCadError};
|
use kicad_ipc::{BoardOriginKind, ClientBuilder, DocumentType, KiCadError, Vector2Nm};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct CliConfig {
|
struct CliConfig {
|
||||||
|
|
@ -15,18 +15,35 @@ struct CliConfig {
|
||||||
enum Command {
|
enum Command {
|
||||||
Ping,
|
Ping,
|
||||||
Version,
|
Version,
|
||||||
OpenDocs { document_type: DocumentType },
|
OpenDocs {
|
||||||
|
document_type: DocumentType,
|
||||||
|
},
|
||||||
ProjectPath,
|
ProjectPath,
|
||||||
BoardOpen,
|
BoardOpen,
|
||||||
Nets,
|
Nets,
|
||||||
EnabledLayers,
|
EnabledLayers,
|
||||||
ActiveLayer,
|
ActiveLayer,
|
||||||
VisibleLayers,
|
VisibleLayers,
|
||||||
BoardOrigin { kind: BoardOriginKind },
|
BoardOrigin {
|
||||||
|
kind: BoardOriginKind,
|
||||||
|
},
|
||||||
SelectionSummary,
|
SelectionSummary,
|
||||||
SelectionDetails,
|
SelectionDetails,
|
||||||
SelectionRaw,
|
SelectionRaw,
|
||||||
NetlistPads,
|
NetlistPads,
|
||||||
|
ItemsById {
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
},
|
||||||
|
ItemBBox {
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
include_child_text: bool,
|
||||||
|
},
|
||||||
|
HitTest {
|
||||||
|
item_id: String,
|
||||||
|
x_nm: i64,
|
||||||
|
y_nm: i64,
|
||||||
|
tolerance_nm: i32,
|
||||||
|
},
|
||||||
Smoke,
|
Smoke,
|
||||||
Help,
|
Help,
|
||||||
}
|
}
|
||||||
|
|
@ -209,6 +226,42 @@ async fn run() -> Result<(), KiCadError> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Command::ItemsById { item_ids } => {
|
||||||
|
let details = client.get_items_by_id_details(item_ids).await?;
|
||||||
|
println!("items_total={}", details.len());
|
||||||
|
for (index, item) in details.iter().enumerate() {
|
||||||
|
println!(
|
||||||
|
"[{index}] type_url={} raw_len={} detail={}",
|
||||||
|
item.type_url, item.raw_len, item.detail
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::ItemBBox {
|
||||||
|
item_ids,
|
||||||
|
include_child_text,
|
||||||
|
} => {
|
||||||
|
let boxes = client
|
||||||
|
.get_item_bounding_boxes(item_ids, include_child_text)
|
||||||
|
.await?;
|
||||||
|
println!("bbox_total={}", boxes.len());
|
||||||
|
for entry in boxes {
|
||||||
|
println!(
|
||||||
|
"item_id={} x_nm={} y_nm={} width_nm={} height_nm={}",
|
||||||
|
entry.item_id, entry.x_nm, entry.y_nm, entry.width_nm, entry.height_nm
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Command::HitTest {
|
||||||
|
item_id,
|
||||||
|
x_nm,
|
||||||
|
y_nm,
|
||||||
|
tolerance_nm,
|
||||||
|
} => {
|
||||||
|
let result = client
|
||||||
|
.hit_test_item(item_id, Vector2Nm { x_nm, y_nm }, tolerance_nm)
|
||||||
|
.await?;
|
||||||
|
println!("hit_test={result}");
|
||||||
|
}
|
||||||
Command::Smoke => {
|
Command::Smoke => {
|
||||||
client.ping().await?;
|
client.ping().await?;
|
||||||
let version = client.get_version().await?;
|
let version = client.get_version().await?;
|
||||||
|
|
@ -300,6 +353,105 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> {
|
||||||
"selection-details" => Command::SelectionDetails,
|
"selection-details" => Command::SelectionDetails,
|
||||||
"selection-raw" => Command::SelectionRaw,
|
"selection-raw" => Command::SelectionRaw,
|
||||||
"netlist-pads" => Command::NetlistPads,
|
"netlist-pads" => Command::NetlistPads,
|
||||||
|
"items-by-id" => {
|
||||||
|
let item_ids = parse_item_ids(&args[1..], "items-by-id")?;
|
||||||
|
Command::ItemsById { item_ids }
|
||||||
|
}
|
||||||
|
"item-bbox" => {
|
||||||
|
let mut item_ids = Vec::new();
|
||||||
|
let mut include_child_text = false;
|
||||||
|
let mut i = 1;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--id" => {
|
||||||
|
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "missing value for item-bbox --id".to_string(),
|
||||||
|
})?;
|
||||||
|
item_ids.push(value.clone());
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
"--include-text" => {
|
||||||
|
include_child_text = true;
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Err(KiCadError::Config {
|
||||||
|
reason: "item-bbox requires one or more `--id <uuid>` arguments".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::ItemBBox {
|
||||||
|
item_ids,
|
||||||
|
include_child_text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"hit-test" => {
|
||||||
|
let mut item_id = None;
|
||||||
|
let mut x_nm = None;
|
||||||
|
let mut y_nm = None;
|
||||||
|
let mut tolerance_nm = 0_i32;
|
||||||
|
let mut i = 1;
|
||||||
|
while i < args.len() {
|
||||||
|
match args[i].as_str() {
|
||||||
|
"--id" => {
|
||||||
|
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "missing value for hit-test --id".to_string(),
|
||||||
|
})?;
|
||||||
|
item_id = Some(value.clone());
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
"--x-nm" => {
|
||||||
|
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "missing value for hit-test --x-nm".to_string(),
|
||||||
|
})?;
|
||||||
|
x_nm = Some(value.parse::<i64>().map_err(|err| KiCadError::Config {
|
||||||
|
reason: format!("invalid hit-test --x-nm `{value}`: {err}"),
|
||||||
|
})?);
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
"--y-nm" => {
|
||||||
|
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "missing value for hit-test --y-nm".to_string(),
|
||||||
|
})?;
|
||||||
|
y_nm = Some(value.parse::<i64>().map_err(|err| KiCadError::Config {
|
||||||
|
reason: format!("invalid hit-test --y-nm `{value}`: {err}"),
|
||||||
|
})?);
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
"--tolerance-nm" => {
|
||||||
|
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "missing value for hit-test --tolerance-nm".to_string(),
|
||||||
|
})?;
|
||||||
|
tolerance_nm = value.parse::<i32>().map_err(|err| KiCadError::Config {
|
||||||
|
reason: format!("invalid hit-test --tolerance-nm `{value}`: {err}"),
|
||||||
|
})?;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::HitTest {
|
||||||
|
item_id: item_id.ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "hit-test requires `--id <uuid>`".to_string(),
|
||||||
|
})?,
|
||||||
|
x_nm: x_nm.ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "hit-test requires `--x-nm <value>`".to_string(),
|
||||||
|
})?,
|
||||||
|
y_nm: y_nm.ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "hit-test requires `--y-nm <value>`".to_string(),
|
||||||
|
})?,
|
||||||
|
tolerance_nm,
|
||||||
|
}
|
||||||
|
}
|
||||||
"smoke" => Command::Smoke,
|
"smoke" => Command::Smoke,
|
||||||
"open-docs" => {
|
"open-docs" => {
|
||||||
let mut document_type = DocumentType::Pcb;
|
let mut document_type = DocumentType::Pcb;
|
||||||
|
|
@ -338,10 +490,34 @@ fn default_config() -> CliConfig {
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] <command> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <type>] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n enabled-layers List enabled board layers\n active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type <t>] Show board origin (`grid` default, or `drill`)\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n"
|
"kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] <command> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <type>] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id <uuid> ... Show parsed details for specific item IDs\n item-bbox --id <uuid> ... Show bounding boxes for item IDs\n hit-test --id <uuid> --x-nm <x> --y-nm <y> [--tolerance-nm <n>]\n Hit-test one item at a point\n enabled-layers List enabled board layers\n active-layer Show active board layer\n visible-layers Show currently visible board layers\n board-origin [--type <t>] Show board origin (`grid` default, or `drill`)\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_item_ids(args: &[String], command_name: &str) -> Result<Vec<String>, KiCadError> {
|
||||||
|
let mut item_ids = Vec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
while i < args.len() {
|
||||||
|
if args[i] == "--id" {
|
||||||
|
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: format!("missing value for {command_name} --id"),
|
||||||
|
})?;
|
||||||
|
item_ids.push(value.clone());
|
||||||
|
i += 2;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Err(KiCadError::Config {
|
||||||
|
reason: format!("{command_name} requires one or more `--id <uuid>` arguments"),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(item_ids)
|
||||||
|
}
|
||||||
|
|
||||||
fn bytes_to_hex(data: &[u8]) -> String {
|
fn bytes_to_hex(data: &[u8]) -> String {
|
||||||
let mut output = String::with_capacity(data.len() * 2);
|
let mut output = String::with_capacity(data.len() * 2);
|
||||||
for byte in data {
|
for byte in data {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue