feat: add pad polygon and padstack presence read APIs
This commit is contained in:
parent
59bc0e7838
commit
21b66d5823
|
|
@ -143,6 +143,18 @@ Dump raw payloads for all PCB object classes:
|
||||||
cargo run --bin kicad-ipc-cli -- items-raw-all-pcb --debug
|
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 <uuid> --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 <uuid> --layer-id 3 --debug
|
||||||
|
```
|
||||||
|
|
||||||
Dump board text (KiCad s-expression):
|
Dump board text (KiCad s-expression):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
@ -184,9 +196,13 @@ cargo run --bin kicad-ipc-cli -- proto-coverage-board-read
|
||||||
Generate full board-read reconstruction markdown report:
|
Generate full board-read reconstruction markdown report:
|
||||||
|
|
||||||
```bash
|
```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):
|
Get current project path (derived from open PCB docs):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
331
src/client.rs
331
src/client.rs
|
|
@ -6,7 +6,9 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
use crate::envelope;
|
use crate::envelope;
|
||||||
use crate::error::KiCadError;
|
use crate::error::KiCadError;
|
||||||
use crate::model::board::{
|
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::{
|
use crate::model::common::{
|
||||||
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
|
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: &str = "kiapi.board.commands.GetItemsByNet";
|
||||||
const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass";
|
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_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_SELECTION: &str = "kiapi.common.commands.GetSelection";
|
||||||
const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
|
const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
|
||||||
const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById";
|
const CMD_GET_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 =
|
const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||||
"kiapi.board.commands.BoardEditorAppearanceSettings";
|
"kiapi.board.commands.BoardEditorAppearanceSettings";
|
||||||
const RES_NETCLASS_FOR_NETS_RESPONSE: &str = "kiapi.board.commands.NetClassForNetsResponse";
|
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_VECTOR2: &str = "kiapi.common.types.Vector2";
|
||||||
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
||||||
const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
|
const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
|
||||||
|
|
@ -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_DOCUMENT_RESPONSE: &str = "kiapi.common.commands.SavedDocumentResponse";
|
||||||
const RES_SAVED_SELECTION_RESPONSE: &str = "kiapi.common.commands.SavedSelectionResponse";
|
const RES_SAVED_SELECTION_RESPONSE: &str = "kiapi.common.commands.SavedSelectionResponse";
|
||||||
|
|
||||||
|
const PAD_QUERY_CHUNK_SIZE: usize = 256;
|
||||||
|
|
||||||
const PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
|
const PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
|
||||||
PcbObjectTypeCode {
|
PcbObjectTypeCode {
|
||||||
code: common_types::KiCadObjectType::KotPcbFootprint as i32,
|
code: common_types::KiCadObjectType::KotPcbFootprint as i32,
|
||||||
|
|
@ -563,6 +572,126 @@ impl KiCadClient {
|
||||||
Ok(format!("{:#?}", payload.classes))
|
Ok(format!("{:#?}", payload.classes))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn get_pad_shape_as_polygon(
|
||||||
|
&self,
|
||||||
|
pad_ids: Vec<String>,
|
||||||
|
layer_id: i32,
|
||||||
|
) -> Result<Vec<PadShapeAsPolygonEntry>, 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<String>,
|
||||||
|
layer_id: i32,
|
||||||
|
) -> Result<String, KiCadError> {
|
||||||
|
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<String>,
|
||||||
|
layer_ids: Vec<i32>,
|
||||||
|
) -> Result<Vec<PadstackPresenceEntry>, 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<String>,
|
||||||
|
layer_ids: Vec<i32>,
|
||||||
|
) -> Result<String, KiCadError> {
|
||||||
|
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<String, KiCadError> {
|
pub async fn get_board_stackup_debug(&self) -> Result<String, KiCadError> {
|
||||||
let command = board_commands::GetBoardStackup {
|
let command = board_commands::GetBoardStackup {
|
||||||
board: Some(self.current_board_document_proto().await?),
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
|
@ -825,6 +954,55 @@ impl KiCadClient {
|
||||||
ensure_item_request_ok(payload.status)?;
|
ensure_item_request_ok(payload.status)?;
|
||||||
Ok(payload.items)
|
Ok(payload.items)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn request_pad_shape_as_polygon(
|
||||||
|
&self,
|
||||||
|
board: &common_types::DocumentSpecifier,
|
||||||
|
pad_ids: &[String],
|
||||||
|
layer_id: i32,
|
||||||
|
) -> Result<board_commands::PadShapeAsPolygonResponse, KiCadError> {
|
||||||
|
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<board_commands::PadstackPresenceResponse, KiCadError> {
|
||||||
|
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<DocumentSpecifier> {
|
fn map_document_specifier(source: common_types::DocumentSpecifier) -> Option<DocumentSpecifier> {
|
||||||
|
|
@ -979,6 +1157,66 @@ fn map_hit_test_result(value: i32) -> ItemHitTestResult {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn map_polygon_with_holes(
|
||||||
|
polygon: common_types::PolygonWithHoles,
|
||||||
|
) -> Result<PolygonWithHolesNm, KiCadError> {
|
||||||
|
Ok(PolygonWithHolesNm {
|
||||||
|
outline: polygon.outline.map(map_polyline).transpose()?,
|
||||||
|
holes: polygon
|
||||||
|
.holes
|
||||||
|
.into_iter()
|
||||||
|
.map(map_polyline)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_polyline(line: common_types::PolyLine) -> Result<PolyLineNm, KiCadError> {
|
||||||
|
Ok(PolyLineNm {
|
||||||
|
nodes: line
|
||||||
|
.nodes
|
||||||
|
.into_iter()
|
||||||
|
.map(map_polyline_node)
|
||||||
|
.collect::<Result<Vec<_>, _>>()?,
|
||||||
|
closed: line.closed,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn map_polyline_node(
|
||||||
|
node: common_types::PolyLineNode,
|
||||||
|
) -> Result<PolyLineNodeGeometryNm, KiCadError> {
|
||||||
|
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<T: prost::Message + Default>(
|
fn decode_any<T: prost::Message + Default>(
|
||||||
payload: &prost_types::Any,
|
payload: &prost_types::Any,
|
||||||
expected_type_name: &str,
|
expected_type_name: &str,
|
||||||
|
|
@ -1473,9 +1711,10 @@ fn default_client_name() -> String {
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
any_to_pretty_debug, ensure_item_request_ok, layer_to_model, map_hit_test_result,
|
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,
|
map_item_bounding_boxes, map_polygon_with_holes, model_document_to_proto,
|
||||||
pad_netlist_from_footprint_items, select_single_board_document, select_single_project_path,
|
normalize_socket_uri, pad_netlist_from_footprint_items, select_single_board_document,
|
||||||
selection_item_detail, summarize_item_details, summarize_selection, PCB_OBJECT_TYPES,
|
select_single_project_path, selection_item_detail, summarize_item_details,
|
||||||
|
summarize_selection, PCB_OBJECT_TYPES,
|
||||||
};
|
};
|
||||||
use crate::error::KiCadError;
|
use crate::error::KiCadError;
|
||||||
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
use crate::model::common::{DocumentSpecifier, DocumentType, ProjectInfo};
|
||||||
|
|
@ -1839,4 +2078,88 @@ mod tests {
|
||||||
assert!(debug.contains("unparsed_any"));
|
assert!(debug.contains("unparsed_any"));
|
||||||
assert!(debug.contains("raw_len=4"));
|
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 { .. }));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,9 @@ pub(crate) mod proto;
|
||||||
pub use crate::client::{ClientBuilder, KiCadClient};
|
pub use crate::client::{ClientBuilder, KiCadClient};
|
||||||
pub use crate::error::KiCadError;
|
pub use crate::error::KiCadError;
|
||||||
pub use crate::model::board::{
|
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::{
|
pub use crate::model::common::{
|
||||||
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
|
DocumentSpecifier, DocumentType, ItemBoundingBox, ItemHitTestResult, PcbObjectTypeCode,
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,47 @@ pub struct PadNetEntry {
|
||||||
pub net_name: Option<String>,
|
pub net_name: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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<PolyLineNodeGeometryNm>,
|
||||||
|
pub closed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
|
pub struct PolygonWithHolesNm {
|
||||||
|
pub outline: Option<PolyLineNm>,
|
||||||
|
pub holes: Vec<PolyLineNm>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
@ -9,6 +10,12 @@ use kicad_ipc::{
|
||||||
Vector2Nm,
|
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)]
|
#[derive(Debug)]
|
||||||
struct CliConfig {
|
struct CliConfig {
|
||||||
socket: Option<String>,
|
socket: Option<String>,
|
||||||
|
|
@ -57,6 +64,16 @@ enum Command {
|
||||||
ItemsRawAllPcb {
|
ItemsRawAllPcb {
|
||||||
include_debug: bool,
|
include_debug: bool,
|
||||||
},
|
},
|
||||||
|
PadShapePolygon {
|
||||||
|
pad_ids: Vec<String>,
|
||||||
|
layer_id: i32,
|
||||||
|
include_debug: bool,
|
||||||
|
},
|
||||||
|
PadstackPresence {
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
layer_ids: Vec<i32>,
|
||||||
|
include_debug: bool,
|
||||||
|
},
|
||||||
TitleBlock,
|
TitleBlock,
|
||||||
BoardAsString,
|
BoardAsString,
|
||||||
SelectionAsString,
|
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 => {
|
Command::TitleBlock => {
|
||||||
let title_block = client.get_title_block_info().await?;
|
let title_block = client.get_title_block_info().await?;
|
||||||
println!("title={}", title_block.title);
|
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");
|
let include_debug = args.iter().any(|arg| arg == "--debug");
|
||||||
Command::ItemsRawAllPcb { include_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::<i32>().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 <uuid>` arguments"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::PadShapePolygon {
|
||||||
|
pad_ids,
|
||||||
|
layer_id: layer_id.ok_or_else(|| KiCadError::Config {
|
||||||
|
reason: "pad-shape-polygon requires `--layer-id <i32>`".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::<i32>().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 <uuid>` arguments"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if layer_ids.is_empty() {
|
||||||
|
return Err(KiCadError::Config {
|
||||||
|
reason: "padstack-presence requires one or more `--layer-id <i32>` arguments"
|
||||||
|
.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Command::PadstackPresence {
|
||||||
|
item_ids,
|
||||||
|
layer_ids,
|
||||||
|
include_debug,
|
||||||
|
}
|
||||||
|
}
|
||||||
"title-block" => Command::TitleBlock,
|
"title-block" => Command::TitleBlock,
|
||||||
"board-as-string" => Command::BoardAsString,
|
"board-as-string" => Command::BoardAsString,
|
||||||
"selection-as-string" => Command::SelectionAsString,
|
"selection-as-string" => Command::SelectionAsString,
|
||||||
|
|
@ -704,13 +890,13 @@ fn default_config() -> CliConfig {
|
||||||
CliConfig {
|
CliConfig {
|
||||||
socket: None,
|
socket: None,
|
||||||
token: None,
|
token: None,
|
||||||
timeout_ms: 3_000,
|
timeout_ms: 15_000,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_help() {
|
fn print_help() {
|
||||||
println!(
|
println!(
|
||||||
"kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--timeout-ms N] <command> [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n open-docs [--type <type>] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n 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"
|
"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 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-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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -756,12 +942,13 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
|
||||||
|
|
||||||
out.push_str("## Layer / Origin / Nets\n\n");
|
out.push_str("## Layer / Origin / Nets\n\n");
|
||||||
let enabled = client.get_board_enabled_layers().await?;
|
let enabled = client.get_board_enabled_layers().await?;
|
||||||
|
let enabled_layers = enabled.layers.clone();
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"- copper_layer_count: {}\n",
|
"- copper_layer_count: {}\n",
|
||||||
enabled.copper_layer_count
|
enabled.copper_layer_count
|
||||||
));
|
));
|
||||||
out.push_str("- enabled_layers:\n");
|
out.push_str("- enabled_layers:\n");
|
||||||
for layer in enabled.layers {
|
for layer in &enabled_layers {
|
||||||
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
|
out.push_str(&format!(" - {} ({})\n", layer.name, layer.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -802,8 +989,15 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
|
||||||
|
|
||||||
out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n");
|
out.push_str("### Pad-Level Netlist (Footprint/Pad/Net)\n\n");
|
||||||
let pad_entries = client.get_pad_netlist().await?;
|
let pad_entries = client.get_pad_netlist().await?;
|
||||||
|
let mut pad_ids = BTreeSet::new();
|
||||||
out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len()));
|
out.push_str(&format!("- pad_entry_count: {}\n", pad_entries.len()));
|
||||||
for entry in pad_entries {
|
for (index, entry) in pad_entries.iter().enumerate() {
|
||||||
|
if let Some(id) = entry.pad_id.as_ref() {
|
||||||
|
pad_ids.insert(id.clone());
|
||||||
|
}
|
||||||
|
if index >= REPORT_MAX_PAD_NET_ROWS {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"- footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}\n",
|
"- footprint_ref={} footprint_id={} pad_id={} pad_number={} net_code={} net_name={}\n",
|
||||||
entry.footprint_reference.as_deref().unwrap_or("-"),
|
entry.footprint_reference.as_deref().unwrap_or("-"),
|
||||||
|
|
@ -817,8 +1011,99 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
|
||||||
entry.net_name.as_deref().unwrap_or("-")
|
entry.net_name.as_deref().unwrap_or("-")
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if pad_entries.len() > 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');
|
out.push('\n');
|
||||||
|
|
||||||
|
let pad_ids: Vec<String> = pad_ids.into_iter().collect();
|
||||||
|
let enabled_layer_ids: Vec<i32> = 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<i32, BTreeSet<String>> = 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::<Vec<_>>())
|
||||||
|
.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("## Board/Editor Raw Structures\n\n");
|
||||||
out.push_str("### Title Block\n\n");
|
out.push_str("### Title Block\n\n");
|
||||||
let title_block = client.get_title_block_info().await?;
|
let title_block = client.get_title_block_info().await?;
|
||||||
|
|
@ -864,7 +1149,16 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
|
||||||
}
|
}
|
||||||
out.push_str(&format!("- status: ok\n- count: {}\n\n", items.len()));
|
out.push_str(&format!("- status: ok\n- count: {}\n\n", items.len()));
|
||||||
|
|
||||||
for (index, item) in items.iter().enumerate() {
|
for (index, item) in items
|
||||||
|
.iter()
|
||||||
|
.take(REPORT_MAX_ITEM_DEBUG_ROWS_PER_TYPE)
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let mut debug = kicad_ipc::KiCadClient::debug_any_item(item)?;
|
||||||
|
if debug.len() > REPORT_MAX_ITEM_DEBUG_CHARS {
|
||||||
|
debug.truncate(REPORT_MAX_ITEM_DEBUG_CHARS);
|
||||||
|
debug.push_str("\n...<truncated; use items-raw CLI for full payload>");
|
||||||
|
}
|
||||||
out.push_str(&format!(
|
out.push_str(&format!(
|
||||||
"#### item {}\n\n- type_url: `{}`\n- raw_len: `{}`\n\n",
|
"#### item {}\n\n- type_url: `{}`\n- raw_len: `{}`\n\n",
|
||||||
index,
|
index,
|
||||||
|
|
@ -872,9 +1166,17 @@ async fn build_board_read_report_markdown(client: &KiCadClient) -> Result<String
|
||||||
item.value.len()
|
item.value.len()
|
||||||
));
|
));
|
||||||
out.push_str("```text\n");
|
out.push_str("```text\n");
|
||||||
out.push_str(&kicad_ipc::KiCadClient::debug_any_item(item)?);
|
out.push_str(&debug);
|
||||||
out.push_str("\n```\n\n");
|
out.push_str("\n```\n\n");
|
||||||
}
|
}
|
||||||
|
if items.len() > 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) => {
|
Err(err) => {
|
||||||
out.push_str(&format!("- status: error\n- error: `{}`\n\n", 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<String
|
||||||
}
|
}
|
||||||
|
|
||||||
out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n");
|
out.push_str("## Board File Snapshot (Raw)\n\n```scheme\n");
|
||||||
out.push_str(&client.get_board_as_string().await?);
|
let mut board_text = client.get_board_as_string().await?;
|
||||||
|
if board_text.len() > REPORT_MAX_BOARD_SNAPSHOT_CHARS {
|
||||||
|
board_text.truncate(REPORT_MAX_BOARD_SNAPSHOT_CHARS);
|
||||||
|
board_text.push_str(
|
||||||
|
"\n... ; <truncated board snapshot, rerun `board-as-string` command for full board text>\n",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
out.push_str(&board_text);
|
||||||
out.push_str("\n```\n\n");
|
out.push_str("\n```\n\n");
|
||||||
|
|
||||||
out.push_str("## Proto Coverage (Board Read)\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",
|
"kiapi.board.commands.GetPadShapeAsPolygon",
|
||||||
"not-yet",
|
"implemented",
|
||||||
"pending",
|
"get_pad_shape_as_polygon/get_pad_shape_as_polygon_debug",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"kiapi.board.commands.CheckPadstackPresenceOnLayers",
|
"kiapi.board.commands.CheckPadstackPresenceOnLayers",
|
||||||
"not-yet",
|
"implemented",
|
||||||
"pending",
|
"check_padstack_presence_on_layers/check_padstack_presence_on_layers_debug",
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"kiapi.board.commands.GetVisibleLayers",
|
"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<Vec<String>, KiCadError> {
|
fn parse_item_ids(args: &[String], command_name: &str) -> Result<Vec<String>, KiCadError> {
|
||||||
let mut item_ids = Vec::new();
|
let mut item_ids = Vec::new();
|
||||||
let mut i = 0;
|
let mut i = 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue