diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 522a4be..9ff231e 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -143,6 +143,18 @@ Dump raw payloads for all PCB object classes: cargo run --bin kicad-ipc-cli -- items-raw-all-pcb --debug ``` +Check whether pads/vias have flashed padstack shapes on specific layers: + +```bash +cargo run --bin kicad-ipc-cli -- padstack-presence --item-id --layer-id 3 --layer-id 34 --debug +``` + +Get polygonized pad shape(s) on a specific layer: + +```bash +cargo run --bin kicad-ipc-cli -- pad-shape-polygon --pad-id --layer-id 3 --debug +``` + Dump board text (KiCad s-expression): ```bash @@ -184,9 +196,13 @@ 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 +cargo run --bin kicad-ipc-cli -- --timeout-ms 60000 board-read-report --out docs/BOARD_READ_REPORT.md ``` +Notes: +- Report output is intentionally capped for very large boards to avoid multi-GB files. +- For full raw payloads, use targeted commands such as `items-raw --debug`, `pad-shape-polygon --debug`, and `padstack-presence --debug`. + Get current project path (derived from open PCB docs): ```bash diff --git a/src/client.rs b/src/client.rs index f3b0698..1e86c76 100644 --- a/src/client.rs +++ b/src/client.rs @@ -6,7 +6,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::envelope; use crate::error::KiCadError; use crate::model::board::{ - BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm, + ArcStartMidEndNm, BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, + PadShapeAsPolygonEntry, PadstackPresenceEntry, PolyLineNm, PolyLineNodeGeometryNm, + PolygonWithHolesNm, Vector2Nm, }; use crate::model::common::{ DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode, @@ -37,6 +39,9 @@ const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = 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_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPolygon"; +const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str = + "kiapi.board.commands.CheckPadstackPresenceOnLayers"; 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"; @@ -57,6 +62,8 @@ const RES_GRAPHICS_DEFAULTS_RESPONSE: &str = "kiapi.board.commands.GraphicsDefau const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str = "kiapi.board.commands.BoardEditorAppearanceSettings"; const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse"; +const RES_PAD_SHAPE_AS_POLYGON_RESPONSE: &str = "kiapi.board.commands.PadShapeAsPolygonResponse"; +const RES_PADSTACK_PRESENCE_RESPONSE: &str = "kiapi.board.commands.PadstackPresenceResponse"; 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"; @@ -66,6 +73,8 @@ 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 PAD_QUERY_CHUNK_SIZE: usize = 256; + const PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [ PcbObjectTypeCode { code: common_types::KiCadObjectType::KotPcbFootprint as i32, @@ -563,6 +572,126 @@ impl KiCadClient { Ok(format!("{:#?}", payload.classes)) } + pub async fn get_pad_shape_as_polygon( + &self, + pad_ids: Vec, + layer_id: i32, + ) -> Result, KiCadError> { + if pad_ids.is_empty() { + return Ok(Vec::new()); + } + + let board = self.current_board_document_proto().await?; + let mut entries = Vec::new(); + let layer_name = layer_to_model(layer_id).name; + + for chunk in pad_ids.chunks(PAD_QUERY_CHUNK_SIZE) { + let payload = self + .request_pad_shape_as_polygon(&board, chunk, layer_id) + .await?; + + if payload.pads.len() != payload.polygons.len() { + return Err(KiCadError::InvalidResponse { + reason: format!( + "GetPadShapeAsPolygon returned mismatched arrays: pads={}, polygons={}", + payload.pads.len(), + payload.polygons.len() + ), + }); + } + + for (pad, polygon) in payload.pads.into_iter().zip(payload.polygons.into_iter()) { + entries.push(PadShapeAsPolygonEntry { + pad_id: pad.value, + layer_id, + layer_name: layer_name.clone(), + polygon: map_polygon_with_holes(polygon)?, + }); + } + } + + Ok(entries) + } + + pub async fn get_pad_shape_as_polygon_debug( + &self, + pad_ids: Vec, + layer_id: i32, + ) -> Result { + if pad_ids.is_empty() { + return Ok("PadShapeAsPolygonResponse { pads: [], polygons: [] }".to_string()); + } + + let board = self.current_board_document_proto().await?; + let mut debug_chunks = Vec::new(); + for chunk in pad_ids.chunks(PAD_QUERY_CHUNK_SIZE) { + let payload = self + .request_pad_shape_as_polygon(&board, chunk, layer_id) + .await?; + debug_chunks.push(format!("{:#?}", payload)); + } + + Ok(debug_chunks.join("\n\n")) + } + + pub async fn check_padstack_presence_on_layers( + &self, + item_ids: Vec, + layer_ids: Vec, + ) -> Result, KiCadError> { + if item_ids.is_empty() || layer_ids.is_empty() { + return Ok(Vec::new()); + } + + let board = self.current_board_document_proto().await?; + let mut entries = Vec::new(); + for chunk in item_ids.chunks(PAD_QUERY_CHUNK_SIZE) { + let payload = self + .request_padstack_presence_on_layers(&board, chunk, &layer_ids) + .await?; + for row in payload.entries { + let item = row.item.ok_or_else(|| KiCadError::InvalidResponse { + reason: "PadstackPresenceEntry missing item id".to_string(), + })?; + + let layer = layer_to_model(row.layer); + let presence = board_commands::PadstackPresence::try_from(row.presence) + .map(|value| value.as_str_name().to_string()) + .unwrap_or_else(|_| format!("UNKNOWN({})", row.presence)); + + entries.push(PadstackPresenceEntry { + item_id: item.value, + layer_id: row.layer, + layer_name: layer.name, + presence, + }); + } + } + + Ok(entries) + } + + pub async fn check_padstack_presence_on_layers_debug( + &self, + item_ids: Vec, + layer_ids: Vec, + ) -> Result { + if item_ids.is_empty() || layer_ids.is_empty() { + return Ok("PadstackPresenceResponse { entries: [] }".to_string()); + } + + let board = self.current_board_document_proto().await?; + let mut debug_chunks = Vec::new(); + for chunk in item_ids.chunks(PAD_QUERY_CHUNK_SIZE) { + let payload = self + .request_padstack_presence_on_layers(&board, chunk, &layer_ids) + .await?; + debug_chunks.push(format!("{:#?}", payload)); + } + + Ok(debug_chunks.join("\n\n")) + } + pub async fn get_board_stackup_debug(&self) -> Result { let command = board_commands::GetBoardStackup { board: Some(self.current_board_document_proto().await?), @@ -825,6 +954,55 @@ impl KiCadClient { ensure_item_request_ok(payload.status)?; Ok(payload.items) } + + async fn request_pad_shape_as_polygon( + &self, + board: &common_types::DocumentSpecifier, + pad_ids: &[String], + layer_id: i32, + ) -> Result { + let command = board_commands::GetPadShapeAsPolygon { + board: Some(board.clone()), + pads: pad_ids + .iter() + .cloned() + .map(|value| common_types::Kiid { value }) + .collect(), + layer: layer_id, + }; + + let response = self + .send_command(envelope::pack_any(&command, CMD_GET_PAD_SHAPE_AS_POLYGON)) + .await?; + + envelope::unpack_any(&response, RES_PAD_SHAPE_AS_POLYGON_RESPONSE) + } + + async fn request_padstack_presence_on_layers( + &self, + board: &common_types::DocumentSpecifier, + item_ids: &[String], + layer_ids: &[i32], + ) -> Result { + let command = board_commands::CheckPadstackPresenceOnLayers { + board: Some(board.clone()), + items: item_ids + .iter() + .cloned() + .map(|value| common_types::Kiid { value }) + .collect(), + layers: layer_ids.to_vec(), + }; + + let response = self + .send_command(envelope::pack_any( + &command, + CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS, + )) + .await?; + + envelope::unpack_any(&response, RES_PADSTACK_PRESENCE_RESPONSE) + } } fn map_document_specifier(source: common_types::DocumentSpecifier) -> Option { @@ -979,6 +1157,66 @@ fn map_hit_test_result(value: i32) -> ItemHitTestResult { } } +fn map_polygon_with_holes( + polygon: common_types::PolygonWithHoles, +) -> Result { + Ok(PolygonWithHolesNm { + outline: polygon.outline.map(map_polyline).transpose()?, + holes: polygon + .holes + .into_iter() + .map(map_polyline) + .collect::, _>>()?, + }) +} + +fn map_polyline(line: common_types::PolyLine) -> Result { + Ok(PolyLineNm { + nodes: line + .nodes + .into_iter() + .map(map_polyline_node) + .collect::, _>>()?, + closed: line.closed, + }) +} + +fn map_polyline_node( + node: common_types::PolyLineNode, +) -> Result { + match node.geometry { + Some(common_types::poly_line_node::Geometry::Point(point)) => { + Ok(PolyLineNodeGeometryNm::Point(map_vector2_nm(point))) + } + Some(common_types::poly_line_node::Geometry::Arc(arc)) => { + let start = arc.start.ok_or_else(|| KiCadError::InvalidResponse { + reason: "polyline arc node missing start point".to_string(), + })?; + let mid = arc.mid.ok_or_else(|| KiCadError::InvalidResponse { + reason: "polyline arc node missing mid point".to_string(), + })?; + let end = arc.end.ok_or_else(|| KiCadError::InvalidResponse { + reason: "polyline arc node missing end point".to_string(), + })?; + Ok(PolyLineNodeGeometryNm::Arc(ArcStartMidEndNm { + start: map_vector2_nm(start), + mid: map_vector2_nm(mid), + end: map_vector2_nm(end), + })) + } + None => Err(KiCadError::InvalidResponse { + reason: "polyline node has no geometry".to_string(), + }), + } +} + +fn map_vector2_nm(value: common_types::Vector2) -> Vector2Nm { + Vector2Nm { + x_nm: value.x_nm, + y_nm: value.y_nm, + } +} + fn decode_any( payload: &prost_types::Any, expected_type_name: &str, @@ -1473,9 +1711,10 @@ fn default_client_name() -> String { mod tests { use super::{ 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, + map_item_bounding_boxes, map_polygon_with_holes, 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}; @@ -1839,4 +2078,88 @@ mod tests { assert!(debug.contains("unparsed_any")); assert!(debug.contains("raw_len=4")); } + + #[test] + fn map_polygon_with_holes_maps_points_and_arcs() { + let polygon = crate::proto::kiapi::common::types::PolygonWithHoles { + outline: Some(crate::proto::kiapi::common::types::PolyLine { + nodes: vec![ + crate::proto::kiapi::common::types::PolyLineNode { + geometry: Some( + crate::proto::kiapi::common::types::poly_line_node::Geometry::Point( + crate::proto::kiapi::common::types::Vector2 { x_nm: 10, y_nm: 20 }, + ), + ), + }, + crate::proto::kiapi::common::types::PolyLineNode { + geometry: Some( + crate::proto::kiapi::common::types::poly_line_node::Geometry::Arc( + crate::proto::kiapi::common::types::ArcStartMidEnd { + start: Some(crate::proto::kiapi::common::types::Vector2 { + x_nm: 0, + y_nm: 0, + }), + mid: Some(crate::proto::kiapi::common::types::Vector2 { + x_nm: 5, + y_nm: 5, + }), + end: Some(crate::proto::kiapi::common::types::Vector2 { + x_nm: 10, + y_nm: 0, + }), + }, + ), + ), + }, + ], + closed: true, + }), + holes: vec![crate::proto::kiapi::common::types::PolyLine { + nodes: vec![crate::proto::kiapi::common::types::PolyLineNode { + geometry: Some( + crate::proto::kiapi::common::types::poly_line_node::Geometry::Point( + crate::proto::kiapi::common::types::Vector2 { x_nm: 1, y_nm: 1 }, + ), + ), + }], + closed: true, + }], + }; + + let mapped = map_polygon_with_holes(polygon).expect("polygon mapping should succeed"); + let outline = mapped.outline.expect("outline should be present"); + assert_eq!(outline.nodes.len(), 2); + assert!(outline.closed); + assert_eq!(mapped.holes.len(), 1); + } + + #[test] + fn map_polygon_with_holes_rejects_missing_arc_points() { + let polygon = crate::proto::kiapi::common::types::PolygonWithHoles { + outline: Some(crate::proto::kiapi::common::types::PolyLine { + nodes: vec![crate::proto::kiapi::common::types::PolyLineNode { + geometry: Some( + crate::proto::kiapi::common::types::poly_line_node::Geometry::Arc( + crate::proto::kiapi::common::types::ArcStartMidEnd { + start: Some(crate::proto::kiapi::common::types::Vector2 { + x_nm: 0, + y_nm: 0, + }), + mid: None, + end: Some(crate::proto::kiapi::common::types::Vector2 { + x_nm: 10, + y_nm: 0, + }), + }, + ), + ), + }], + closed: false, + }), + holes: Vec::new(), + }; + + let err = map_polygon_with_holes(polygon).expect_err("missing arc point must fail"); + assert!(matches!(err, KiCadError::InvalidResponse { .. })); + } } diff --git a/src/lib.rs b/src/lib.rs index d3be28a..2ec04d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,9 @@ pub(crate) mod proto; pub use crate::client::{ClientBuilder, KiCadClient}; pub use crate::error::KiCadError; pub use crate::model::board::{ - BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, Vector2Nm, + ArcStartMidEndNm, BoardEnabledLayers, BoardLayerInfo, BoardNet, BoardOriginKind, PadNetEntry, + PadShapeAsPolygonEntry, PadstackPresenceEntry, PolyLineNm, PolyLineNodeGeometryNm, + PolygonWithHolesNm, Vector2Nm, }; pub use crate::model::common::{ DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode, diff --git a/src/model/board.rs b/src/model/board.rs index c998574..9b876c3 100644 --- a/src/model/board.rs +++ b/src/model/board.rs @@ -63,6 +63,47 @@ pub struct PadNetEntry { pub net_name: Option, } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ArcStartMidEndNm { + pub start: Vector2Nm, + pub mid: Vector2Nm, + pub end: Vector2Nm, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum PolyLineNodeGeometryNm { + Point(Vector2Nm), + Arc(ArcStartMidEndNm), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PolyLineNm { + pub nodes: Vec, + pub closed: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PolygonWithHolesNm { + pub outline: Option, + pub holes: Vec, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PadShapeAsPolygonEntry { + pub pad_id: String, + pub layer_id: i32, + pub layer_name: String, + pub polygon: PolygonWithHolesNm, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PadstackPresenceEntry { + pub item_id: String, + pub layer_id: i32, + pub layer_name: String, + pub presence: String, +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 9f7bb99..23b3e2d 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -1,3 +1,4 @@ +use std::collections::{BTreeMap, BTreeSet}; use std::fs; use std::path::PathBuf; use std::process::ExitCode; @@ -9,6 +10,12 @@ use kicad_ipc::{ Vector2Nm, }; +const REPORT_MAX_PAD_NET_ROWS: usize = 2_000; +const REPORT_MAX_PRESENCE_ROWS: usize = 2_000; +const REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE: usize = 5; +const REPORT_MAX_ITEM_DEBUG_CHARS: usize = 8_000; +const REPORT_MAX_BOARD_SNAPSHOT_CHARS: usize = 750_000; + #[derive(Debug)] struct CliConfig { socket: Option, @@ -57,6 +64,16 @@ enum Command { ItemsRawAllPcb { include_debug: bool, }, + PadShapePolygon { + pad_ids: Vec, + layer_id: i32, + include_debug: bool, + }, + PadstackPresence { + item_ids: Vec, + layer_ids: Vec, + include_debug: bool, + }, TitleBlock, BoardAsString, SelectionAsString, @@ -369,6 +386,70 @@ async fn run() -> Result<(), KiCadError> { } } } + Command::PadShapePolygon { + pad_ids, + layer_id, + include_debug, + } => { + let rows = client + .get_pad_shape_as_polygon(pad_ids.clone(), layer_id) + .await?; + println!( + "pad_shape_total={} layer_id={} requested_pad_count={}", + rows.len(), + layer_id, + pad_ids.len() + ); + for row in &rows { + let outline_nodes = row + .polygon + .outline + .as_ref() + .map(|outline| outline.nodes.len()) + .unwrap_or(0); + println!( + "pad_id={} layer_id={} layer_name={} outline_nodes={} hole_count={}", + row.pad_id, + row.layer_id, + row.layer_name, + outline_nodes, + row.polygon.holes.len() + ); + } + if include_debug { + let debug = client + .get_pad_shape_as_polygon_debug(pad_ids, layer_id) + .await?; + println!("debug={}", debug.replace('\n', "\\n").replace('\t', " ")); + } + } + Command::PadstackPresence { + item_ids, + layer_ids, + include_debug, + } => { + let rows = client + .check_padstack_presence_on_layers(item_ids.clone(), layer_ids.clone()) + .await?; + println!( + "padstack_presence_total={} requested_item_count={} requested_layer_count={}", + rows.len(), + item_ids.len(), + layer_ids.len() + ); + for row in &rows { + println!( + "item_id={} layer_id={} layer_name={} presence={}", + row.item_id, row.layer_id, row.layer_name, row.presence + ); + } + if include_debug { + let debug = client + .check_padstack_presence_on_layers_debug(item_ids, layer_ids) + .await?; + println!("debug={}", debug.replace('\n', "\\n").replace('\t', " ")); + } + } Command::TitleBlock => { let title_block = client.get_title_block_info().await?; println!("title={}", title_block.title); @@ -648,6 +729,111 @@ fn parse_args() -> Result<(CliConfig, Command), KiCadError> { let include_debug = args.iter().any(|arg| arg == "--debug"); Command::ItemsRawAllPcb { include_debug } } + "pad-shape-polygon" => { + let mut pad_ids = Vec::new(); + let mut layer_id = None; + let mut include_debug = false; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--pad-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for pad-shape-polygon --pad-id".to_string(), + })?; + pad_ids.push(value.clone()); + i += 2; + } + "--layer-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for pad-shape-polygon --layer-id".to_string(), + })?; + layer_id = + Some(value.parse::().map_err(|err| KiCadError::Config { + reason: format!( + "invalid pad-shape-polygon --layer-id `{value}`: {err}" + ), + })?); + i += 2; + } + "--debug" => { + include_debug = true; + i += 1; + } + _ => { + i += 1; + } + } + } + + if pad_ids.is_empty() { + return Err(KiCadError::Config { + reason: "pad-shape-polygon requires one or more `--pad-id ` arguments" + .to_string(), + }); + } + + Command::PadShapePolygon { + pad_ids, + layer_id: layer_id.ok_or_else(|| KiCadError::Config { + reason: "pad-shape-polygon requires `--layer-id `".to_string(), + })?, + include_debug, + } + } + "padstack-presence" => { + let mut item_ids = Vec::new(); + let mut layer_ids = Vec::new(); + let mut include_debug = false; + let mut i = 1; + while i < args.len() { + match args[i].as_str() { + "--item-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for padstack-presence --item-id".to_string(), + })?; + item_ids.push(value.clone()); + i += 2; + } + "--layer-id" => { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for padstack-presence --layer-id".to_string(), + })?; + layer_ids.push(value.parse::().map_err(|err| KiCadError::Config { + reason: format!( + "invalid padstack-presence --layer-id `{value}`: {err}" + ), + })?); + i += 2; + } + "--debug" => { + include_debug = true; + i += 1; + } + _ => { + i += 1; + } + } + } + + if item_ids.is_empty() { + return Err(KiCadError::Config { + reason: "padstack-presence requires one or more `--item-id ` arguments" + .to_string(), + }); + } + if layer_ids.is_empty() { + return Err(KiCadError::Config { + reason: "padstack-presence requires one or more `--layer-id ` arguments" + .to_string(), + }); + } + + Command::PadstackPresence { + item_ids, + layer_ids, + include_debug, + } + } "title-block" => Command::TitleBlock, "board-as-string" => Command::BoardAsString, "selection-as-string" => Command::SelectionAsString, @@ -704,13 +890,13 @@ fn default_config() -> CliConfig { CliConfig { socket: None, token: None, - timeout_ms: 3_000, + timeout_ms: 15_000, } } fn print_help() { println!( - "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--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 ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\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 ... 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 ] 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 options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--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 ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\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 ... 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 ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--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-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 ] 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" ); } @@ -756,12 +942,13 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result Result= REPORT_MAX_PAD_NET_ROWS { + continue; + } out.push_str(&format!( "- footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}\n", entry.footprint_reference.as_deref().unwrap_or("-"), @@ -817,8 +1011,99 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result REPORT_MAX_PAD_NET_ROWS { + out.push_str(&format!( + "- ... omitted {} additional pad net rows (use `netlist-pads` CLI command for full output)\n", + pad_entries.len() - REPORT_MAX_PAD_NET_ROWS + )); + } out.push('\n'); + let pad_ids: Vec = pad_ids.into_iter().collect(); + let enabled_layer_ids: Vec = enabled_layers.iter().map(|layer| layer.id).collect(); + + out.push_str("### Padstack Presence Matrix (Pad IDs x Enabled Layers)\n\n"); + out.push_str(&format!( + "- unique_pad_id_count: {}\n- enabled_layer_count: {}\n", + pad_ids.len(), + enabled_layer_ids.len() + )); + + let mut present_pad_ids_by_layer: BTreeMap> = BTreeMap::new(); + let presence_rows = client + .check_padstack_presence_on_layers(pad_ids.clone(), enabled_layer_ids) + .await?; + out.push_str(&format!( + "- presence_entry_count: {}\n", + presence_rows.len() + )); + for row in &presence_rows { + if row.presence == "PSP_PRESENT" { + present_pad_ids_by_layer + .entry(row.layer_id) + .or_default() + .insert(row.item_id.clone()); + } + } + for (index, row) in presence_rows.iter().enumerate() { + if index >= REPORT_MAX_PRESENCE_ROWS { + continue; + } + out.push_str(&format!( + "- item_id={} layer_id={} layer_name={} presence={}\n", + row.item_id, row.layer_id, row.layer_name, row.presence + )); + } + if presence_rows.len() > REPORT_MAX_PRESENCE_ROWS { + out.push_str(&format!( + "- ... omitted {} additional presence rows (use `padstack-presence` CLI command for full output)\n", + presence_rows.len() - REPORT_MAX_PRESENCE_ROWS + )); + } + out.push('\n'); + + out.push_str("### Pad Shape Polygons (All Present Pad/Layer Pairs)\n\n"); + out.push_str( + "For full per-node coordinate payloads, run `pad-shape-polygon --pad-id ... --layer-id ... --debug` for targeted pad/layer subsets.\n\n", + ); + for layer in &enabled_layers { + let pad_ids_on_layer = present_pad_ids_by_layer + .get(&layer.id) + .map(|set| set.iter().cloned().collect::>()) + .unwrap_or_default(); + + out.push_str(&format!( + "#### Layer {} ({})\n\n- pad_count_present: {}\n\n", + layer.name, + layer.id, + pad_ids_on_layer.len() + )); + + if pad_ids_on_layer.is_empty() { + continue; + } + + let polygons = client + .get_pad_shape_as_polygon(pad_ids_on_layer, layer.id) + .await?; + out.push_str(&format!("- polygon_entry_count: {}\n\n", polygons.len())); + for row in polygons { + let summary = polygon_geometry_summary(&row.polygon); + out.push_str(&format!( + "- pad_id={} layer_id={} layer_name={} outline_nodes={} hole_count={} hole_nodes_total={} point_nodes={} arc_nodes={}\n", + row.pad_id, + row.layer_id, + row.layer_name, + summary.outline_nodes, + summary.hole_count, + summary.hole_nodes_total, + summary.point_nodes, + summary.arc_nodes + )); + } + 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?; @@ -864,7 +1149,16 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result REPORT_MAX_ITEM_DEBUG_CHARS { + debug.truncate(REPORT_MAX_ITEM_DEBUG_CHARS); + debug.push_str("\n..."); + } out.push_str(&format!( "#### item {}\n\n- type_url: `{}`\n- raw_len: `{}`\n\n", index, @@ -872,9 +1166,17 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE { + out.push_str(&format!( + "- ... omitted {} additional item debug rows for {} (use `items-raw --type-id {}` for full output)\n\n", + items.len() - REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE, + object_type.name, + object_type.code + )); + } } Err(err) => { out.push_str(&format!("- status: error\n- error: `{}`\n\n", err)); @@ -896,7 +1198,14 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result REPORT_MAX_BOARD_SNAPSHOT_CHARS { + board_text.truncate(REPORT_MAX_BOARD_SNAPSHOT_CHARS); + board_text.push_str( + "\n... ; \n", + ); + } + out.push_str(&board_text); out.push_str("\n```\n\n"); out.push_str("## Proto Coverage (Board Read)\n\n"); @@ -954,13 +1263,13 @@ fn proto_coverage_board_read_rows() -> Vec<(&'static str, &'static str, &'static ), ( "kiapi.board.commands.GetPadShapeAsPolygon", - "not-yet", - "pending", + "implemented", + "get_pad_shape_as_polygon/get_pad_shape_as_polygon_debug", ), ( "kiapi.board.commands.CheckPadstackPresenceOnLayers", - "not-yet", - "pending", + "implemented", + "check_padstack_presence_on_layers/check_padstack_presence_on_layers_debug", ), ( "kiapi.board.commands.GetVisibleLayers", @@ -1025,6 +1334,44 @@ fn proto_coverage_board_read_rows() -> Vec<(&'static str, &'static str, &'static ] } +#[derive(Default)] +struct PolygonGeometrySummary { + outline_nodes: usize, + hole_count: usize, + hole_nodes_total: usize, + point_nodes: usize, + arc_nodes: usize, +} + +fn polygon_geometry_summary(polygon: &kicad_ipc::PolygonWithHolesNm) -> PolygonGeometrySummary { + let mut summary = PolygonGeometrySummary { + hole_count: polygon.holes.len(), + ..PolygonGeometrySummary::default() + }; + + if let Some(outline) = polygon.outline.as_ref() { + summary.outline_nodes = outline.nodes.len(); + for node in &outline.nodes { + match node { + kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, + kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, + } + } + } + + for hole in &polygon.holes { + summary.hole_nodes_total += hole.nodes.len(); + for node in &hole.nodes { + match node { + kicad_ipc::PolyLineNodeGeometryNm::Point(_) => summary.point_nodes += 1, + kicad_ipc::PolyLineNodeGeometryNm::Arc(_) => summary.arc_nodes += 1, + } + } + } + + summary +} + fn parse_item_ids(args: &[String], command_name: &str) -> Result, KiCadError> { let mut item_ids = Vec::new(); let mut i = 0;