feat: add board reconstruction verification report tooling

This commit is contained in:
Milind Sharma 2026-02-19 12:24:12 +08:00
parent 1b54e688c1
commit 59bc0e7838
6 changed files with 83483 additions and 12 deletions

82305
docs/BOARD_READ_REPORT.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -125,6 +125,68 @@ Run hit-test on a specific item:
cargo run --bin kicad-ipc-cli -- hit-test --id <uuid> --x-nm <x> --y-nm <y> --tolerance-nm 0
```
List all PCB object type IDs from the proto enum:
```bash
cargo run --bin kicad-ipc-cli -- types-pcb
```
Dump raw item payloads for one or more PCB object type IDs:
```bash
cargo run --bin kicad-ipc-cli -- items-raw --type-id 11 --type-id 13 --debug
```
Dump raw payloads for all PCB object classes:
```bash
cargo run --bin kicad-ipc-cli -- items-raw-all-pcb --debug
```
Dump board text (KiCad s-expression):
```bash
cargo run --bin kicad-ipc-cli -- board-as-string
```
Dump selection text (KiCad s-expression):
```bash
cargo run --bin kicad-ipc-cli -- selection-as-string
```
Dump title block fields:
```bash
cargo run --bin kicad-ipc-cli -- title-block
```
Dump stackup/graphics/appearance raw debug:
```bash
cargo run --bin kicad-ipc-cli -- stackup-debug
cargo run --bin kicad-ipc-cli -- graphics-defaults-debug
cargo run --bin kicad-ipc-cli -- appearance-debug
```
Dump netclass map raw debug:
```bash
cargo run --bin kicad-ipc-cli -- netclass-debug
```
Print proto command coverage status (board read):
```bash
cargo run --bin kicad-ipc-cli -- proto-coverage-board-read
```
Generate full board-read reconstruction markdown report:
```bash
cargo run --bin kicad-ipc-cli -- board-read-report --out docs/BOARD_READ_REPORT.md
```
Get current project path (derived from open PCB docs):
```bash

View File

@ -9,8 +9,9 @@ use crate::model::board::{
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
};
use crate::model::common::{
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, ProjectInfo,
SelectionItemDetail, SelectionSummary, SelectionTypeCount, VersionInfo,
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
ProjectInfo, SelectionItemDetail, SelectionSummary, SelectionTypeCount, TitleBlockInfo,
VersionInfo,
};
use crate::proto::kiapi::board::commands as board_commands;
use crate::proto::kiapi::board::types as board_types;
@ -29,11 +30,21 @@ const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabled
const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer";
const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers";
const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup";
const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults";
const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
"kiapi.board.commands.GetBoardEditorAppearanceSettings";
const CMD_GET_ITEMS_BY_NET: &str = "kiapi.board.commands.GetItemsByNet";
const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass";
const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets";
const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
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 CMD_GET_TITLE_BLOCK_INFO: &str = "kiapi.common.commands.GetTitleBlockInfo";
const CMD_SAVE_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToString";
const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString";
const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
@ -41,11 +52,94 @@ const RES_GET_NETS: &str = "kiapi.board.commands.NetsResponse";
const RES_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.BoardEnabledLayersResponse";
const RES_BOARD_LAYER_RESPONSE: &str = "kiapi.board.commands.BoardLayerResponse";
const RES_BOARD_LAYERS: &str = "kiapi.board.commands.BoardLayers";
const RES_BOARD_STACKUP_RESPONSE: &str = "kiapi.board.commands.BoardStackupResponse";
const RES_GRAPHICS_DEFAULTS_RESPONSE: &str = "kiapi.board.commands.GraphicsDefaultsResponse";
const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
"kiapi.board.commands.BoardEditorAppearanceSettings";
const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse";
const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
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";
const RES_TITLE_BLOCK_INFO: &str = "kiapi.common.types.TitleBlockInfo";
const RES_SAVED_DOCUMENT_RESPONSE: &str = "kiapi.common.commands.SavedDocumentResponse";
const RES_SAVED_SELECTION_RESPONSE: &str = "kiapi.common.commands.SavedSelectionResponse";
const PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbFootprint as i32,
name: "KOT_PCB_FOOTPRINT",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbPad as i32,
name: "KOT_PCB_PAD",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbShape as i32,
name: "KOT_PCB_SHAPE",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbReferenceImage as i32,
name: "KOT_PCB_REFERENCE_IMAGE",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbField as i32,
name: "KOT_PCB_FIELD",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbGenerator as i32,
name: "KOT_PCB_GENERATOR",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbText as i32,
name: "KOT_PCB_TEXT",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbTextbox as i32,
name: "KOT_PCB_TEXTBOX",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbTable as i32,
name: "KOT_PCB_TABLE",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbTablecell as i32,
name: "KOT_PCB_TABLECELL",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbTrace as i32,
name: "KOT_PCB_TRACE",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbVia as i32,
name: "KOT_PCB_VIA",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbArc as i32,
name: "KOT_PCB_ARC",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbMarker as i32,
name: "KOT_PCB_MARKER",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbDimension as i32,
name: "KOT_PCB_DIMENSION",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbZone as i32,
name: "KOT_PCB_ZONE",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbGroup as i32,
name: "KOT_PCB_GROUP",
},
PcbObjectTypeCode {
code: common_types::KiCadObjectType::KotPcbBarcode as i32,
name: "KOT_PCB_BARCODE",
},
];
#[derive(Clone, Debug)]
pub struct KiCadClient {
@ -350,6 +444,224 @@ impl KiCadClient {
pad_netlist_from_footprint_items(footprint_items)
}
pub fn pcb_object_type_codes() -> &'static [PcbObjectTypeCode] {
&PCB_OBJECT_TYPES
}
pub fn pcb_object_type_name(type_code: i32) -> Option<&'static str> {
PCB_OBJECT_TYPES
.iter()
.find(|entry| entry.code == type_code)
.map(|entry| entry.name)
}
pub fn debug_any_item(item: &prost_types::Any) -> Result<String, KiCadError> {
any_to_pretty_debug(item)
}
pub async fn get_items_raw_by_type_codes(
&self,
type_codes: Vec<i32>,
) -> Result<Vec<prost_types::Any>, KiCadError> {
self.get_items_raw(type_codes).await
}
pub async fn get_items_details_by_type_codes(
&self,
type_codes: Vec<i32>,
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
let items = self.get_items_raw(type_codes).await?;
summarize_item_details(items)
}
pub async fn get_all_pcb_items_raw(
&self,
) -> Result<Vec<(PcbObjectTypeCode, Vec<prost_types::Any>)>, KiCadError> {
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
for object_type in PCB_OBJECT_TYPES {
let items = self.get_items_raw(vec![object_type.code]).await?;
rows.push((object_type, items));
}
Ok(rows)
}
pub async fn get_all_pcb_items_details(
&self,
) -> Result<Vec<(PcbObjectTypeCode, Vec<SelectionItemDetail>)>, KiCadError> {
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
for object_type in PCB_OBJECT_TYPES {
let items = self.get_items_raw(vec![object_type.code]).await?;
rows.push((object_type, summarize_item_details(items)?));
}
Ok(rows)
}
pub async fn get_items_by_net_raw(
&self,
type_codes: Vec<i32>,
net_codes: Vec<i32>,
) -> Result<Vec<prost_types::Any>, KiCadError> {
let command = board_commands::GetItemsByNet {
header: Some(self.current_board_item_header().await?),
types: type_codes,
net_codes: net_codes
.into_iter()
.map(|value| board_types::NetCode { value })
.collect(),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_NET))
.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_net_class_raw(
&self,
type_codes: Vec<i32>,
net_classes: Vec<String>,
) -> Result<Vec<prost_types::Any>, KiCadError> {
let command = board_commands::GetItemsByNetClass {
header: Some(self.current_board_item_header().await?),
types: type_codes,
net_classes,
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_NET_CLASS))
.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_netclass_for_nets_debug(
&self,
nets: Vec<BoardNet>,
) -> Result<String, KiCadError> {
let command = board_commands::GetNetClassForNets {
net: nets
.into_iter()
.map(|net| board_types::Net {
code: Some(board_types::NetCode { value: net.code }),
name: net.name,
})
.collect(),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_NETCLASS_FOR_NETS))
.await?;
let payload: board_commands::NetClassForNetsResponse =
envelope::unpack_any(&response, RES_NETCLASS_FOR_NETS_RESPONSE)?;
Ok(format!("{:#?}", payload.classes))
}
pub async fn get_board_stackup_debug(&self) -> Result<String, KiCadError> {
let command = board_commands::GetBoardStackup {
board: Some(self.current_board_document_proto().await?),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_BOARD_STACKUP))
.await?;
let payload: board_commands::BoardStackupResponse =
envelope::unpack_any(&response, RES_BOARD_STACKUP_RESPONSE)?;
Ok(format!("{:#?}", payload.stackup))
}
pub async fn get_graphics_defaults_debug(&self) -> Result<String, KiCadError> {
let command = board_commands::GetGraphicsDefaults {
board: Some(self.current_board_document_proto().await?),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_GRAPHICS_DEFAULTS))
.await?;
let payload: board_commands::GraphicsDefaultsResponse =
envelope::unpack_any(&response, RES_GRAPHICS_DEFAULTS_RESPONSE)?;
Ok(format!("{:#?}", payload.defaults))
}
pub async fn get_board_editor_appearance_settings_debug(&self) -> Result<String, KiCadError> {
let command = board_commands::GetBoardEditorAppearanceSettings {};
let response = self
.send_command(envelope::pack_any(
&command,
CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS,
))
.await?;
let payload: board_commands::BoardEditorAppearanceSettings =
envelope::unpack_any(&response, RES_BOARD_EDITOR_APPEARANCE_SETTINGS)?;
Ok(format!("{:#?}", payload))
}
pub async fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError> {
let command = common_commands::GetTitleBlockInfo {
document: Some(self.current_board_document_proto().await?),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_GET_TITLE_BLOCK_INFO))
.await?;
let payload: common_types::TitleBlockInfo =
envelope::unpack_any(&response, RES_TITLE_BLOCK_INFO)?;
let comments = vec![
payload.comment1,
payload.comment2,
payload.comment3,
payload.comment4,
payload.comment5,
payload.comment6,
payload.comment7,
payload.comment8,
payload.comment9,
]
.into_iter()
.filter(|comment| !comment.is_empty())
.collect();
Ok(TitleBlockInfo {
title: payload.title,
date: payload.date,
revision: payload.revision,
company: payload.company,
comments,
})
}
pub async fn get_board_as_string(&self) -> Result<String, KiCadError> {
let command = common_commands::SaveDocumentToString {
document: Some(self.current_board_document_proto().await?),
};
let response = self
.send_command(envelope::pack_any(&command, CMD_SAVE_DOCUMENT_TO_STRING))
.await?;
let payload: common_commands::SavedDocumentResponse =
envelope::unpack_any(&response, RES_SAVED_DOCUMENT_RESPONSE)?;
Ok(payload.contents)
}
pub async fn get_selection_as_string(&self) -> Result<String, KiCadError> {
let command = common_commands::SaveSelectionToString {};
let response = self
.send_command(envelope::pack_any(&command, CMD_SAVE_SELECTION_TO_STRING))
.await?;
let payload: common_commands::SavedSelectionResponse =
envelope::unpack_any(&response, RES_SAVED_SELECTION_RESPONSE)?;
Ok(payload.contents)
}
pub async fn get_items_by_id_raw(
&self,
item_ids: Vec<String>,
@ -764,6 +1076,49 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
));
}
if item.type_url == envelope::type_url("kiapi.board.types.Arc") {
let arc = decode_any::<board_types::Arc>(item, "kiapi.board.types.Arc")?;
let id = arc.id.map_or_else(|| "-".to_string(), |id| id.value);
let start = arc
.start
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
let mid = arc
.mid
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
let end = arc
.end
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
let width = arc
.width
.map_or_else(|| "-".to_string(), |w| w.value_nm.to_string());
let layer = layer_to_model(arc.layer).name;
let net = arc
.net
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
.unwrap_or_else(|| "-".to_string());
return Ok(format!(
"arc id={id} start_nm={start} mid_nm={mid} end_nm={end} width_nm={width} layer={layer} net={net}"
));
}
if item.type_url == envelope::type_url("kiapi.board.types.Via") {
let via = decode_any::<board_types::Via>(item, "kiapi.board.types.Via")?;
let id = via.id.map_or_else(|| "-".to_string(), |id| id.value);
let position = via
.position
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
let net = via
.net
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
.unwrap_or_else(|| "-".to_string());
let via_type = board_types::ViaType::try_from(via.r#type)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", via.r#type));
return Ok(format!(
"via id={id} pos_nm={position} type={via_type} net={net}"
));
}
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
let fp = decode_any::<board_types::FootprintInstance>(
item,
@ -780,9 +1135,24 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
let position = fp
.position
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
let orientation_deg = fp.orientation.map_or_else(
|| "-".to_string(),
|orientation| orientation.value_degrees.to_string(),
);
let layer = layer_to_model(fp.layer).name;
let pad_count = fp
.definition
.as_ref()
.map(|definition| {
definition
.items
.iter()
.filter(|entry| entry.type_url == envelope::type_url("kiapi.board.types.Pad"))
.count()
})
.unwrap_or(0);
return Ok(format!(
"footprint id={id} ref={reference} pos_nm={position} layer={layer}"
"footprint id={id} ref={reference} pos_nm={position} orientation_deg={orientation_deg} layer={layer} pad_count={pad_count}"
));
}
@ -800,6 +1170,50 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
));
}
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
let text = decode_any::<board_types::BoardText>(item, "kiapi.board.types.BoardText")?;
let id = text.id.map_or_else(|| "-".to_string(), |id| id.value);
let layer = layer_to_model(text.layer).name;
let body = text
.text
.as_ref()
.map(|value| value.text.clone())
.unwrap_or_else(|| "-".to_string());
return Ok(format!("text id={id} layer={layer} text={body}"));
}
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
let textbox =
decode_any::<board_types::BoardTextBox>(item, "kiapi.board.types.BoardTextBox")?;
let id = textbox.id.map_or_else(|| "-".to_string(), |id| id.value);
let layer = layer_to_model(textbox.layer).name;
let body = textbox
.textbox
.as_ref()
.map(|value| value.text.clone())
.unwrap_or_else(|| "-".to_string());
return Ok(format!("textbox id={id} layer={layer} text={body}"));
}
if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
let pad = decode_any::<board_types::Pad>(item, "kiapi.board.types.Pad")?;
let id = pad.id.map_or_else(|| "-".to_string(), |id| id.value);
let pad_type = board_types::PadType::try_from(pad.r#type)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", pad.r#type));
let position = pad
.position
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
let net = pad
.net
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
.unwrap_or_else(|| "-".to_string());
return Ok(format!(
"pad id={id} number={} type={pad_type} pos_nm={position} net={net}",
pad.number
));
}
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
let shape = decode_any::<board_types::BoardGraphicShape>(
item,
@ -811,12 +1225,136 @@ fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError>
.net
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
.unwrap_or_else(|| "-".to_string());
return Ok(format!("graphic id={id} layer={layer} net={net}"));
let geometry = shape
.shape
.as_ref()
.map(|graphic| format!("{:?}", graphic.geometry))
.unwrap_or_else(|| "-".to_string());
return Ok(format!(
"graphic id={id} layer={layer} net={net} geometry={geometry}"
));
}
if item.type_url == envelope::type_url("kiapi.board.types.Zone") {
let zone = decode_any::<board_types::Zone>(item, "kiapi.board.types.Zone")?;
let id = zone.id.map_or_else(|| "-".to_string(), |id| id.value);
let zone_type = board_types::ZoneType::try_from(zone.r#type)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", zone.r#type));
return Ok(format!(
"zone id={id} name={} type={} layer_count={} filled={} polygon_count={}",
zone.name,
zone_type,
zone.layers.len(),
zone.filled,
zone.filled_polygons.len()
));
}
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
let dimension = decode_any::<board_types::Dimension>(item, "kiapi.board.types.Dimension")?;
let id = dimension.id.map_or_else(|| "-".to_string(), |id| id.value);
let layer = layer_to_model(dimension.layer).name;
let text = dimension
.text
.as_ref()
.map(|value| value.text.clone())
.unwrap_or_else(|| "-".to_string());
let style = format!("{:?}", dimension.dimension_style);
return Ok(format!(
"dimension id={id} layer={layer} text={} style={style}",
text
));
}
if item.type_url == envelope::type_url("kiapi.board.types.Group") {
let group = decode_any::<board_types::Group>(item, "kiapi.board.types.Group")?;
let id = group.id.map_or_else(|| "-".to_string(), |id| id.value);
return Ok(format!(
"group id={id} name={} item_count={}",
group.name,
group.items.len()
));
}
Ok(format!("unparsed payload ({} bytes)", item.value.len()))
}
fn any_to_pretty_debug(item: &prost_types::Any) -> Result<String, KiCadError> {
if item.type_url == envelope::type_url("kiapi.board.types.Track") {
let value = decode_any::<board_types::Track>(item, "kiapi.board.types.Track")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Arc") {
let value = decode_any::<board_types::Arc>(item, "kiapi.board.types.Arc")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Via") {
let value = decode_any::<board_types::Via>(item, "kiapi.board.types.Via")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
let value = decode_any::<board_types::FootprintInstance>(
item,
"kiapi.board.types.FootprintInstance",
)?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
let value = decode_any::<board_types::Pad>(item, "kiapi.board.types.Pad")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
let value = decode_any::<board_types::BoardGraphicShape>(
item,
"kiapi.board.types.BoardGraphicShape",
)?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
let value = decode_any::<board_types::BoardText>(item, "kiapi.board.types.BoardText")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
let value =
decode_any::<board_types::BoardTextBox>(item, "kiapi.board.types.BoardTextBox")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Field") {
let value = decode_any::<board_types::Field>(item, "kiapi.board.types.Field")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Zone") {
let value = decode_any::<board_types::Zone>(item, "kiapi.board.types.Zone")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
let value = decode_any::<board_types::Dimension>(item, "kiapi.board.types.Dimension")?;
return Ok(format!("{:#?}", value));
}
if item.type_url == envelope::type_url("kiapi.board.types.Group") {
let value = decode_any::<board_types::Group>(item, "kiapi.board.types.Group")?;
return Ok(format!("{:#?}", value));
}
Ok(format!(
"unparsed_any type_url={} raw_len={}",
item.type_url,
item.value.len()
))
}
fn select_single_board_document(
docs: &[DocumentSpecifier],
) -> Result<&DocumentSpecifier, KiCadError> {
@ -934,10 +1472,10 @@ fn default_client_name() -> String {
#[cfg(test)]
mod tests {
use super::{
ensure_item_request_ok, layer_to_model, map_hit_test_result, map_item_bounding_boxes,
model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items,
select_single_board_document, select_single_project_path, selection_item_detail,
summarize_item_details, summarize_selection,
any_to_pretty_debug, ensure_item_request_ok, layer_to_model, map_hit_test_result,
map_item_bounding_boxes, model_document_to_proto, normalize_socket_uri,
pad_netlist_from_footprint_items, select_single_board_document, select_single_project_path,
selection_item_detail, summarize_item_details, summarize_selection, PCB_OBJECT_TYPES,
};
use crate::error::KiCadError;
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
@ -1281,4 +1819,24 @@ mod tests {
crate::model::common::ItemHitTestResult::NoHit
);
}
#[test]
fn pcb_object_type_catalog_contains_expected_trace_entry() {
assert!(PCB_OBJECT_TYPES
.iter()
.any(|entry| entry.name == "KOT_PCB_TRACE" && entry.code == 11));
}
#[test]
fn any_to_pretty_debug_handles_unknown_type_without_error() {
let unknown = prost_types::Any {
type_url: "type.googleapis.com/kiapi.board.types.DoesNotExist".to_string(),
value: vec![0xde, 0xad, 0xbe, 0xef],
};
let debug = any_to_pretty_debug(&unknown)
.expect("unknown Any payload type should not fail debug rendering");
assert!(debug.contains("unparsed_any"));
assert!(debug.contains("raw_len=4"));
}
}

View File

@ -24,6 +24,6 @@ pub use crate::model::board::{
BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm,
};
pub use crate::model::common::{
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, SelectionItemDetail,
SelectionSummary, SelectionTypeCount, VersionInfo,
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
SelectionItemDetail, SelectionSummary, SelectionTypeCount, TitleBlockInfo, VersionInfo,
};

View File

@ -112,6 +112,15 @@ pub struct SelectionItemDetail {
pub raw_len: usize,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct TitleBlockInfo {
pub title: String,
pub date: String,
pub revision: String,
pub company: String,
pub comments: Vec<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ItemBoundingBox {
pub item_id: String,
@ -128,6 +137,12 @@ pub enum ItemHitTestResult {
Hit,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct PcbObjectTypeCode {
pub code: i32,
pub name: &'static str,
}
impl std::fmt::Display for ItemHitTestResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let value = match self {

View File

@ -1,8 +1,13 @@
use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;
use std::str::FromStr;
use std::time::Duration;
use kicad_ipc::{BoardOriginKind, ClientBuilder, DocumentType, KiCadError, Vector2Nm};
use kicad_ipc::{
BoardOriginKind, ClientBuilder, DocumentType, KiCadClient, KiCadError, PcbObjectTypeCode,
Vector2Nm,
};
#[derive(Debug)]
struct CliConfig {
@ -44,6 +49,25 @@ enum Command {
y_nm: i64,
tolerance_nm: i32,
},
PcbTypes,
ItemsRaw {
type_codes: Vec<i32>,
include_debug: bool,
},
ItemsRawAllPcb {
include_debug: bool,
},
TitleBlock,
BoardAsString,
SelectionAsString,
StackupDebug,
GraphicsDefaultsDebug,
AppearanceDebug,
NetClassDebug,
BoardReadReport {
output: PathBuf,
},
ProtoCoverageBoardRead,
Smoke,
Help,
}
@ -262,6 +286,134 @@ async fn run() -> Result<(), KiCadError> {
.await?;
println!("hit_test={result}");
}
Command::PcbTypes => {
for entry in kicad_ipc::KiCadClient::pcb_object_type_codes() {
println!("type_id={} type_name={}", entry.code, entry.name);
}
}
Command::ItemsRaw {
type_codes,
include_debug,
} => {
let items = client
.get_items_raw_by_type_codes(type_codes.clone())
.await?;
println!(
"items_total={} requested_type_codes={:?}",
items.len(),
type_codes
);
for (index, item) in items.iter().enumerate() {
if include_debug {
let debug = kicad_ipc::KiCadClient::debug_any_item(item)?
.replace('\n', "\\n")
.replace('\t', " ");
println!(
"[{index}] type_url={} raw_len={} raw_hex={} debug={}",
item.type_url,
item.value.len(),
bytes_to_hex(&item.value),
debug
);
} else {
println!(
"[{index}] type_url={} raw_len={} raw_hex={}",
item.type_url,
item.value.len(),
bytes_to_hex(&item.value)
);
}
}
}
Command::ItemsRawAllPcb { include_debug } => {
for object_type in kicad_ipc::KiCadClient::pcb_object_type_codes() {
match client
.get_items_raw_by_type_codes(vec![object_type.code])
.await
{
Ok(items) => {
println!(
"type_id={} type_name={} item_count={}",
object_type.code,
object_type.name,
items.len()
);
for (index, item) in items.iter().enumerate() {
if include_debug {
let debug = kicad_ipc::KiCadClient::debug_any_item(item)?
.replace('\n', "\\n")
.replace('\t', " ");
println!(
" [{index}] type_url={} raw_len={} raw_hex={} debug={}",
item.type_url,
item.value.len(),
bytes_to_hex(&item.value),
debug
);
} else {
println!(
" [{index}] type_url={} raw_len={} raw_hex={}",
item.type_url,
item.value.len(),
bytes_to_hex(&item.value)
);
}
}
}
Err(err) => {
println!(
"type_id={} type_name={} error={}",
object_type.code, object_type.name, err
);
}
}
}
}
Command::TitleBlock => {
let title_block = client.get_title_block_info().await?;
println!("title={}", title_block.title);
println!("date={}", title_block.date);
println!("revision={}", title_block.revision);
println!("company={}", title_block.company);
for (index, comment) in title_block.comments.iter().enumerate() {
println!("comment{}={}", index + 1, comment);
}
}
Command::BoardAsString => {
let content = client.get_board_as_string().await?;
println!("{content}");
}
Command::SelectionAsString => {
let content = client.get_selection_as_string().await?;
println!("{content}");
}
Command::StackupDebug => {
let debug = client.get_board_stackup_debug().await?;
println!("{debug}");
}
Command::GraphicsDefaultsDebug => {
let debug = client.get_graphics_defaults_debug().await?;
println!("{debug}");
}
Command::AppearanceDebug => {
let debug = client.get_board_editor_appearance_settings_debug().await?;
println!("{debug}");
}
Command::NetClassDebug => {
let nets = client.get_nets().await?;
let debug = client.get_netclass_for_nets_debug(nets).await?;
println!("{debug}");
}
Command::BoardReadReport { output } => {
let report = build_board_read_report_markdown(&client).await?;
fs::write(&output, report).map_err(|err| KiCadError::Config {
reason: format!("failed to write report to `{}`: {err}", output.display()),
})?;
println!("wrote_report={}", output.display());
}
Command::ProtoCoverageBoardRead => {
print_proto_coverage_board_read();
}
Command::Smoke => {
client.ping().await?;
let version = client.get_version().await?;
@ -452,6 +604,74 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> {
tolerance_nm,
}
}
"types-pcb" => Command::PcbTypes,
"items-raw" => {
let mut type_codes = Vec::new();
let mut include_debug = false;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--type-id" => {
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
reason: "missing value for items-raw --type-id".to_string(),
})?;
type_codes.push(value.parse::<i32>().map_err(|err| {
KiCadError::Config {
reason: format!("invalid items-raw --type-id `{value}`: {err}"),
}
})?);
i += 2;
}
"--debug" => {
include_debug = true;
i += 1;
}
_ => {
i += 1;
}
}
}
if type_codes.is_empty() {
return Err(KiCadError::Config {
reason: "items-raw requires one or more `--type-id <i32>` arguments"
.to_string(),
});
}
Command::ItemsRaw {
type_codes,
include_debug,
}
}
"items-raw-all-pcb" => {
let include_debug = args.iter().any(|arg| arg == "--debug");
Command::ItemsRawAllPcb { include_debug }
}
"title-block" => Command::TitleBlock,
"board-as-string" => Command::BoardAsString,
"selection-as-string" => Command::SelectionAsString,
"stackup-debug" => Command::StackupDebug,
"graphics-defaults-debug" => Command::GraphicsDefaultsDebug,
"appearance-debug" => Command::AppearanceDebug,
"netclass-debug" => Command::NetClassDebug,
"proto-coverage-board-read" => Command::ProtoCoverageBoardRead,
"board-read-report" => {
let mut output = PathBuf::from("docs/BOARD_READ_REPORT.md");
let mut i = 1;
while i < args.len() {
if args[i] == "--out" {
let value = args.get(i + 1).ok_or_else(|| KiCadError::Config {
reason: "missing value for board-read-report --out".to_string(),
})?;
output = PathBuf::from(value);
i += 2;
continue;
}
i += 1;
}
Command::BoardReadReport { output }
}
"smoke" => Command::Smoke,
"open-docs" => {
let mut document_type = DocumentType::Pcb;
@ -490,10 +710,321 @@ fn default_config() -> CliConfig {
fn print_help() {
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 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"
"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 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 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-debug Dump raw stackup response\n graphics-defaults-debug Dump raw graphics defaults response\n appearance-debug Dump raw editor appearance settings response\n netclass-debug Dump raw 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 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"
);
}
async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String, KiCadError> {
let mut out = String::new();
out.push_str("# Board Read Reconstruction Report\n\n");
out.push_str("Generated by `kicad-ipc-cli board-read-report`.\n\n");
out.push_str("Goal: verify that non-mutating PCB API reads are sufficient to reconstruct board state.\n\n");
let version = client.get_version().await?;
out.push_str("## Session\n\n");
out.push_str(&format!(
"- KiCad version: {}.{}.{} ({})\n",
version.major, version.minor, version.patch, version.full_version
));
out.push_str(&format!("- Socket URI: `{}`\n", client.socket_uri()));
out.push_str(&format!(
"- Timeout (ms): {}\n\n",
client.timeout().as_millis()
));
out.push_str("## Open Documents\n\n");
let docs = client.get_open_documents(DocumentType::Pcb).await?;
if docs.is_empty() {
out.push_str("- No open PCB docs\n\n");
} else {
for (index, doc) in docs.iter().enumerate() {
out.push_str(&format!(
"- [{}] type={} board={} project_name={} project_path={}\n",
index,
doc.document_type,
doc.board_filename.as_deref().unwrap_or("-"),
doc.project.name.as_deref().unwrap_or("-"),
doc.project
.path
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "-".to_string())
));
}
out.push('\n');
}
out.push_str("## Layer / Origin / Nets\n\n");
let enabled = client.get_board_enabled_layers().await?;
out.push_str(&format!(
"- copper_layer_count: {}\n",
enabled.copper_layer_count
));
out.push_str("- enabled_layers:\n");
for layer in enabled.layers {
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
}
let visible_layers = client.get_visible_layers().await?;
out.push_str("- visible_layers:\n");
for layer in visible_layers {
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
}
let active_layer = client.get_active_layer().await?;
out.push_str(&format!(
"- active_layer: {} ({})\n",
active_layer.name, active_layer.id
));
let grid_origin = client
.get_board_origin(kicad_ipc::BoardOriginKind::Grid)
.await?;
out.push_str(&format!(
"- grid_origin_nm: {},{}\n",
grid_origin.x_nm, grid_origin.y_nm
));
let drill_origin = client
.get_board_origin(kicad_ipc::BoardOriginKind::Drill)
.await?;
out.push_str(&format!(
"- drill_origin_nm: {},{}\n",
drill_origin.x_nm, drill_origin.y_nm
));
let nets = client.get_nets().await?;
out.push_str(&format!("- net_count: {}\n", nets.len()));
out.push_str("\n### Netlist\n\n");
for net in &nets {
out.push_str(&format!("- code={} name={}\n", net.code, net.name));
}
out.push('\n');
out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n");
let pad_entries = client.get_pad_netlist().await?;
out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len()));
for entry in pad_entries {
out.push_str(&format!(
"- footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}\n",
entry.footprint_reference.as_deref().unwrap_or("-"),
entry.footprint_id.as_deref().unwrap_or("-"),
entry.pad_id.as_deref().unwrap_or("-"),
entry.pad_number,
entry
.net_code
.map(|value| value.to_string())
.unwrap_or_else(|| "-".to_string()),
entry.net_name.as_deref().unwrap_or("-")
));
}
out.push('\n');
out.push_str("## Board/Editor Raw Structures\n\n");
out.push_str("### Title Block\n\n");
let title_block = client.get_title_block_info().await?;
out.push_str(&format!("- title: {}\n", title_block.title));
out.push_str(&format!("- date: {}\n", title_block.date));
out.push_str(&format!("- revision: {}\n", title_block.revision));
out.push_str(&format!("- company: {}\n", title_block.company));
for (index, comment) in title_block.comments.iter().enumerate() {
out.push_str(&format!("- comment{}: {}\n", index + 1, comment));
}
out.push('\n');
out.push_str("### Stackup (Raw Debug)\n\n```text\n");
out.push_str(&client.get_board_stackup_debug().await?);
out.push_str("\n```\n\n");
out.push_str("### Graphics Defaults (Raw Debug)\n\n```text\n");
out.push_str(&client.get_graphics_defaults_debug().await?);
out.push_str("\n```\n\n");
out.push_str("### Editor Appearance (Raw Debug)\n\n```text\n");
out.push_str(&client.get_board_editor_appearance_settings_debug().await?);
out.push_str("\n```\n\n");
out.push_str("### NetClass Map (Raw Debug)\n\n```text\n");
out.push_str(&client.get_netclass_for_nets_debug(nets).await?);
out.push_str("\n```\n\n");
out.push_str("## PCB Item Coverage (All KOT_PCB_* Types)\n\n");
let mut missing_types: Vec<PcbObjectTypeCode> = Vec::new();
for object_type in kicad_ipc::KiCadClient::pcb_object_type_codes() {
out.push_str(&format!(
"### {} ({})\n\n",
object_type.name, object_type.code
));
match client
.get_items_raw_by_type_codes(vec![object_type.code])
.await
{
Ok(items) => {
if items.is_empty() {
missing_types.push(*object_type);
}
out.push_str(&format!("- status: ok\n- count: {}\n\n", items.len()));
for (index, item) in items.iter().enumerate() {
out.push_str(&format!(
"#### item {}\n\n- type_url: `{}`\n- raw_len: `{}`\n\n",
index,
item.type_url,
item.value.len()
));
out.push_str("```text\n");
out.push_str(&kicad_ipc::KiCadClient::debug_any_item(item)?);
out.push_str("\n```\n\n");
}
}
Err(err) => {
out.push_str(&format!("- status: error\n- error: `{}`\n\n", err));
}
}
}
out.push_str("## Missing Item Classes In Current Board\n\n");
if missing_types.is_empty() {
out.push_str("- none\n\n");
} else {
for object_type in missing_types {
out.push_str(&format!(
"- {} ({}) had zero items in this board\n",
object_type.name, object_type.code
));
}
out.push_str("\nIf these are important for your reconstruction target, open a denser board and rerun this report.\n\n");
}
out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n");
out.push_str(&client.get_board_as_string().await?);
out.push_str("\n```\n\n");
out.push_str("## Proto Coverage (Board Read)\n\n");
for (command, status, note) in proto_coverage_board_read_rows() {
out.push_str(&format!("- `{}` -> `{}` ({})\n", command, status, note));
}
out.push('\n');
Ok(out)
}
fn print_proto_coverage_board_read() {
for (command, status, note) in proto_coverage_board_read_rows() {
println!("command={} status={} note={}", command, status, note);
}
}
fn proto_coverage_board_read_rows() -> Vec<(&'static str, &'static str, &'static str)> {
vec![
(
"kiapi.board.commands.GetBoardStackup",
"implemented",
"get_board_stackup_debug",
),
(
"kiapi.board.commands.GetBoardEnabledLayers",
"implemented",
"get_board_enabled_layers",
),
(
"kiapi.board.commands.GetGraphicsDefaults",
"implemented",
"get_graphics_defaults_debug",
),
(
"kiapi.board.commands.GetBoardOrigin",
"implemented",
"get_board_origin",
),
("kiapi.board.commands.GetNets", "implemented", "get_nets"),
(
"kiapi.board.commands.GetItemsByNet",
"implemented",
"get_items_by_net_raw",
),
(
"kiapi.board.commands.GetItemsByNetClass",
"implemented",
"get_items_by_net_class_raw",
),
(
"kiapi.board.commands.GetNetClassForNets",
"implemented",
"get_netclass_for_nets_debug",
),
(
"kiapi.board.commands.GetPadShapeAsPolygon",
"not-yet",
"pending",
),
(
"kiapi.board.commands.CheckPadstackPresenceOnLayers",
"not-yet",
"pending",
),
(
"kiapi.board.commands.GetVisibleLayers",
"implemented",
"get_visible_layers",
),
(
"kiapi.board.commands.GetActiveLayer",
"implemented",
"get_active_layer",
),
(
"kiapi.board.commands.GetBoardEditorAppearanceSettings",
"implemented",
"get_board_editor_appearance_settings_debug",
),
(
"kiapi.common.commands.GetOpenDocuments",
"implemented",
"get_open_documents",
),
(
"kiapi.common.commands.GetItems",
"implemented",
"get_items_raw_by_type_codes",
),
(
"kiapi.common.commands.GetItemsById",
"implemented",
"get_items_by_id_raw",
),
(
"kiapi.common.commands.GetBoundingBox",
"implemented",
"get_item_bounding_boxes",
),
(
"kiapi.common.commands.GetSelection",
"implemented",
"get_selection_raw/get_selection_details",
),
(
"kiapi.common.commands.HitTest",
"implemented",
"hit_test_item",
),
(
"kiapi.common.commands.GetTitleBlockInfo",
"implemented",
"get_title_block_info",
),
(
"kiapi.common.commands.SaveDocumentToString",
"implemented",
"get_board_as_string",
),
(
"kiapi.common.commands.SaveSelectionToString",
"implemented",
"get_selection_as_string",
),
]
}
fn parse_item_ids(args: &[String], command_name: &str) -> Result<Vec<String>, KiCadError> {
let mut item_ids = Vec::new();
let mut i = 0;