feat(common): add DeleteItems API and CLI command

This commit is contained in:
Milind Sharma 2026-02-20 18:38:48 +08:00
parent b26a04e392
commit a2a3dbc771
4 changed files with 121 additions and 11 deletions

View File

@ -41,6 +41,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un
- `RunAction`
- `CreateItems`
- `UpdateItems`
- `DeleteItems`
## KiCad v10 RC1.1 API Completion Matrix
@ -61,11 +62,11 @@ Legend:
| Section | Proto Commands | Implemented | Coverage |
| --- | ---: | ---: | ---: |
| Common (base) | 6 | 6 | 100% |
| Common editor/document | 23 | 21 | 91% |
| Common editor/document | 23 | 22 | 96% |
| Project manager | 5 | 3 | 60% |
| Board editor (PCB) | 22 | 20 | 91% |
| Schematic editor (dedicated proto commands) | 0 | 0 | n/a |
| **Total** | **56** | **50** | **89%** |
| **Total** | **56** | **51** | **91%** |
### Common (base)
@ -94,7 +95,7 @@ Legend:
| `GetItems` | Implemented | `KiCadClient::get_items_raw_by_type_codes`, `KiCadClient::get_items_by_type_codes`, `KiCadClient::get_items_details_by_type_codes`, `KiCadClient::get_all_pcb_items_raw`, `KiCadClient::get_all_pcb_items`, `KiCadClient::get_all_pcb_items_details`, `KiCadClient::get_pad_netlist` |
| `GetItemsById` | Implemented | `KiCadClient::get_items_by_id_raw`, `KiCadClient::get_items_by_id`, `KiCadClient::get_items_by_id_details` |
| `UpdateItems` | Implemented | `KiCadClient::update_items_raw`, `KiCadClient::update_items` |
| `DeleteItems` | Not yet | - |
| `DeleteItems` | Implemented | `KiCadClient::delete_items_raw`, `KiCadClient::delete_items` |
| `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` |
| `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` |
| `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` |

View File

@ -199,6 +199,12 @@ Update raw Any item payload(s):
cargo run --bin kicad-ipc-cli -- update-items --item type.googleapis.com/kiapi.board.types.Text=<hex_payload>
```
Delete items by ID:
```bash
cargo run --bin kicad-ipc-cli -- delete-items --id <uuid> --id <uuid>
```
Show summary of current PCB selection by item type:
```bash

View File

@ -78,6 +78,7 @@ const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit";
const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit";
const CMD_CREATE_ITEMS: &str = "kiapi.common.commands.CreateItems";
const CMD_UPDATE_ITEMS: &str = "kiapi.common.commands.UpdateItems";
const CMD_DELETE_ITEMS: &str = "kiapi.common.commands.DeleteItems";
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";
@ -118,6 +119,7 @@ const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitRespon
const RES_END_COMMIT_RESPONSE: &str = "kiapi.common.commands.EndCommitResponse";
const RES_CREATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.CreateItemsResponse";
const RES_UPDATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.UpdateItemsResponse";
const RES_DELETE_ITEMS_RESPONSE: &str = "kiapi.common.commands.DeleteItemsResponse";
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";
@ -695,6 +697,44 @@ impl KiCadClient {
.collect()
}
pub async fn delete_items_raw(
&self,
item_ids: Vec<String>,
) -> Result<prost_types::Any, KiCadError> {
let command = common_commands::DeleteItems {
header: Some(self.current_board_item_header().await?),
item_ids: item_ids
.into_iter()
.map(|value| common_types::Kiid { value })
.collect(),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_DELETE_ITEMS))
.await?;
response_payload_as_any(response, RES_DELETE_ITEMS_RESPONSE)
}
pub async fn delete_items(&self, item_ids: Vec<String>) -> Result<Vec<String>, KiCadError> {
let payload = self.delete_items_raw(item_ids).await?;
let response: common_commands::DeleteItemsResponse =
decode_any(&payload, RES_DELETE_ITEMS_RESPONSE)?;
ensure_item_request_ok(response.status)?;
response
.deleted_items
.into_iter()
.map(|row| {
ensure_item_deletion_status_ok(row.status)?;
row.id
.map(|id| id.value)
.ok_or_else(|| KiCadError::InvalidResponse {
reason: "DeleteItemsResponse missing deleted item id".to_string(),
})
})
.collect()
}
pub async fn get_nets(&self) -> Result<Vec<BoardNet>, KiCadError> {
let board = self.current_board_document_proto().await?;
let command = board_commands::GetNets {
@ -2160,6 +2200,19 @@ fn ensure_item_status_ok(status: Option<common_commands::ItemStatus>) -> Result<
Ok(())
}
fn ensure_item_deletion_status_ok(status: i32) -> Result<(), KiCadError> {
let code = common_commands::ItemDeletionStatus::try_from(status)
.unwrap_or(common_commands::ItemDeletionStatus::IdsUnknown);
if code != common_commands::ItemDeletionStatus::IdsOk {
return Err(KiCadError::ItemStatus {
code: code.as_str_name().to_string(),
});
}
Ok(())
}
fn map_item_bounding_boxes(
item_ids: Vec<common_types::Kiid>,
boxes: Vec<common_types::Box2>,
@ -3281,13 +3334,13 @@ fn default_client_name() -> String {
mod tests {
use super::{
any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto,
drc_severity_to_proto, ensure_item_request_ok, ensure_item_status_ok, layer_to_model,
map_commit_session, map_hit_test_result, map_item_bounding_boxes, map_polygon_with_holes,
map_run_action_status, model_document_to_proto, normalize_socket_uri,
pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document,
select_single_project_path, selection_item_detail, summarize_item_details,
summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto,
PCB_OBJECT_TYPES,
drc_severity_to_proto, ensure_item_deletion_status_ok, ensure_item_request_ok,
ensure_item_status_ok, layer_to_model, map_commit_session, map_hit_test_result,
map_item_bounding_boxes, map_polygon_with_holes, map_run_action_status,
model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items,
response_payload_as_any, select_single_board_document, select_single_project_path,
selection_item_detail, summarize_item_details, summarize_selection,
text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES,
};
use crate::error::KiCadError;
use crate::model::common::{
@ -3713,6 +3766,23 @@ mod tests {
}
}
#[test]
fn ensure_item_deletion_status_ok_accepts_ok_and_rejects_non_ok() {
assert!(ensure_item_deletion_status_ok(
crate::proto::kiapi::common::commands::ItemDeletionStatus::IdsOk as i32
)
.is_ok());
let err = ensure_item_deletion_status_ok(
crate::proto::kiapi::common::commands::ItemDeletionStatus::IdsNonexistent as i32,
)
.expect_err("non-OK item deletion status should fail");
match err {
KiCadError::ItemStatus { code } => assert_eq!(code, "IDS_NONEXISTENT"),
_ => panic!("expected item status error"),
}
}
#[test]
fn summarize_item_details_reports_unknown_payload_as_unparsed() {
let items = vec![prost_types::Any {

File diff suppressed because one or more lines are too long