feat(client): add AddToSelection API and CLI command

This commit is contained in:
Milind Sharma 2026-02-20 18:08:45 +08:00
parent 2867c4a874
commit 9dac48ed87
4 changed files with 81 additions and 4 deletions

View File

@ -50,11 +50,11 @@ Legend:
| Section | Proto Commands | Implemented | Coverage | | Section | Proto Commands | Implemented | Coverage |
| --- | ---: | ---: | ---: | | --- | ---: | ---: | ---: |
| Common (base) | 6 | 4 | 67% | | Common (base) | 6 | 4 | 67% |
| Common editor/document | 23 | 13 | 57% | | Common editor/document | 23 | 14 | 61% |
| Project manager | 5 | 3 | 60% | | Project manager | 5 | 3 | 60% |
| Board editor (PCB) | 22 | 20 | 91% | | Board editor (PCB) | 22 | 20 | 91% |
| Schematic editor (dedicated proto commands) | 0 | 0 | n/a | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a |
| **Total** | **56** | **40** | **71%** | | **Total** | **56** | **41** | **73%** |
### Common (base) ### Common (base)
@ -86,7 +86,7 @@ Legend:
| `DeleteItems` | Not yet | - | | `DeleteItems` | Not yet | - |
| `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` |
| `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | | `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` |
| `AddToSelection` | Not yet | - | | `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` |
| `RemoveFromSelection` | Not yet | - | | `RemoveFromSelection` | Not yet | - |
| `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` | | `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` |
| `HitTest` | Implemented | `KiCadClient::hit_test_item` | | `HitTest` | Implemented | `KiCadClient::hit_test_item` |

View File

@ -169,6 +169,12 @@ Show raw protobuf payload bytes for selected items:
cargo run --bin kicad-ipc-cli -- selection-raw cargo run --bin kicad-ipc-cli -- selection-raw
``` ```
Add items to current selection:
```bash
cargo run --bin kicad-ipc-cli -- add-to-selection --id <uuid> --id <uuid>
```
Clear current selection: Clear current selection:
```bash ```bash

View File

@ -68,6 +68,7 @@ const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str =
"kiapi.board.commands.CheckPadstackPresenceOnLayers"; "kiapi.board.commands.CheckPadstackPresenceOnLayers";
const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError"; const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError";
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection"; const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
const CMD_ADD_TO_SELECTION: &str = "kiapi.common.commands.AddToSelection";
const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection"; const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection";
const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit"; const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit";
const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit"; const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit";
@ -730,6 +731,43 @@ impl KiCadClient {
decode_pcb_items(items) decode_pcb_items(items)
} }
pub async fn add_to_selection_raw(
&self,
item_ids: Vec<String>,
) -> Result<Vec<prost_types::Any>, KiCadError> {
let command = common_commands::AddToSelection {
header: Some(self.current_board_item_header().await?),
items: item_ids
.into_iter()
.map(|value| common_types::Kiid { value })
.collect(),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_ADD_TO_SELECTION))
.await?;
match envelope::unpack_any::<common_commands::SelectionResponse>(
&response,
RES_SELECTION_RESPONSE,
) {
Ok(payload) => Ok(payload.items),
Err(KiCadError::UnexpectedPayloadType {
expected_type_url: _,
actual_type_url,
}) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()),
Err(err) => Err(err),
}
}
pub async fn add_to_selection(
&self,
item_ids: Vec<String>,
) -> Result<SelectionSummary, KiCadError> {
let items = self.add_to_selection_raw(item_ids).await?;
Ok(summarize_selection(items))
}
pub async fn clear_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> { pub async fn clear_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
let command = common_commands::ClearSelection { let command = common_commands::ClearSelection {
header: Some(self.current_board_item_header().await?), header: Some(self.current_board_item_header().await?),

View File

@ -84,6 +84,9 @@ enum Command {
action: CommitAction, action: CommitAction,
message: String, message: String,
}, },
AddToSelection {
item_ids: Vec<String>,
},
ClearSelection, ClearSelection,
SelectionSummary, SelectionSummary,
SelectionDetails, SelectionDetails,
@ -419,6 +422,13 @@ async fn run() -> Result<(), KiCadError> {
.await?; .await?;
println!("end_commit=ok action={}", action); println!("end_commit=ok action={}", action);
} }
Command::AddToSelection { item_ids } => {
let summary = client.add_to_selection(item_ids).await?;
println!("selection_total={}", summary.total_items);
for entry in summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count);
}
}
Command::ClearSelection => { Command::ClearSelection => {
let summary = client.clear_selection().await?; let summary = client.clear_selection().await?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", summary.total_items);
@ -1177,6 +1187,10 @@ fn parse_args_from(mut args: Vec<String>) -> Result<(CliConfig, Command), KiCadE
message, message,
} }
} }
"add-to-selection" => {
let item_ids = parse_item_ids(&args[1..], "add-to-selection")?;
Command::AddToSelection { item_ids }
}
"clear-selection" => Command::ClearSelection, "clear-selection" => Command::ClearSelection,
"selection-summary" => Command::SelectionSummary, "selection-summary" => Command::SelectionSummary,
"selection-details" => Command::SelectionDetails, "selection-details" => Command::SelectionDetails,
@ -1643,7 +1657,7 @@ 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] [--client-name NAME] [--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 net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text <value> (repeatable)\n text-extents Measure text bounding box\n Options: --text <value>\n text-as-shapes Convert text to rendered shapes\n Options: --text <value> (repeatable)\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 types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id <id> ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id <uuid> ... --layer-id <i32> [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id <uuid> ... --layer-id <i32> ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display <normal|dimmed|hidden>\n --net-color-display <all|ratsnest|off>\n --board-flip <normal|flipped-x>\n --ratsnest-display <all-layers|visible-layers>\n Set editor appearance settings\n inject-drc-error --severity <s> --message <text> [--x-nm <i64> --y-nm <i64>] [--item-id <uuid> ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n refill-zones [--zone-id <uuid> ...]\n Refill all zones or a provided subset\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count <u32> [--layer-id <i32> ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id <i32>\n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id <i32> ...\n Set visible board layers\n board-origin [--type <t>] Show board origin (`grid` default, or `drill`)\n set-board-origin --type <t> --x-nm <i64> --y-nm <i64>\n Set board origin (`grid` or `drill`)\n refresh-editor [--frame <f>] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id <uuid> [--action <commit|drop>] [--message <text>]\n End staged commit with commit/drop action\n clear-selection Clear current item selection\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] [--client-name NAME] [--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 net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text <value> (repeatable)\n text-extents Measure text bounding box\n Options: --text <value>\n text-as-shapes Convert text to rendered shapes\n Options: --text <value> (repeatable)\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 types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id <id> ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id <uuid> ... --layer-id <i32> [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id <uuid> ... --layer-id <i32> ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display <normal|dimmed|hidden>\n --net-color-display <all|ratsnest|off>\n --board-flip <normal|flipped-x>\n --ratsnest-display <all-layers|visible-layers>\n Set editor appearance settings\n inject-drc-error --severity <s> --message <text> [--x-nm <i64> --y-nm <i64>] [--item-id <uuid> ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n refill-zones [--zone-id <uuid> ...]\n Refill all zones or a provided subset\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count <u32> [--layer-id <i32> ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id <i32>\n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id <i32> ...\n Set visible board layers\n board-origin [--type <t>] Show board origin (`grid` default, or `drill`)\n set-board-origin --type <t> --x-nm <i64> --y-nm <i64>\n Set board origin (`grid` or `drill`)\n refresh-editor [--frame <f>] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id <uuid> [--action <commit|drop>] [--message <text>]\n End staged commit with commit/drop action\n add-to-selection --id <uuid> ...\n Add items to current selection\n clear-selection Clear current item selection\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"
); );
} }
@ -2265,6 +2279,25 @@ mod tests {
assert!(matches!(command, Command::ClearSelection)); assert!(matches!(command, Command::ClearSelection));
} }
#[test]
fn parse_args_parses_add_to_selection() {
let (_, command) = parse_args_from(vec![
"add-to-selection".to_string(),
"--id".to_string(),
"zone-1".to_string(),
"--id".to_string(),
"zone-2".to_string(),
])
.expect("add-to-selection args should parse");
match command {
Command::AddToSelection { item_ids } => {
assert_eq!(item_ids, vec!["zone-1".to_string(), "zone-2".to_string()]);
}
other => panic!("unexpected command variant: {other:?}"),
}
}
#[test] #[test]
fn parse_args_parses_set_active_layer() { fn parse_args_parses_set_active_layer() {
let (_, command) = parse_args_from(vec![ let (_, command) = parse_args_from(vec![