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
|
||||
```
|
||||
|
||||
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):
|
||||
|
||||
```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
|
||||
|
|
|
|||
331
src/client.rs
331
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<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> {
|
||||
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<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> {
|
||||
|
|
@ -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>(
|
||||
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 { .. }));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -63,6 +63,47 @@ pub struct PadNetEntry {
|
|||
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)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
|
|
|||
|
|
@ -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<String>,
|
||||
|
|
@ -57,6 +64,16 @@ enum Command {
|
|||
ItemsRawAllPcb {
|
||||
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,
|
||||
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::<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,
|
||||
"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> [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");
|
||||
let enabled = client.get_board_enabled_layers().await?;
|
||||
let enabled_layers = enabled.layers.clone();
|
||||
out.push_str(&format!(
|
||||
"- copper_layer_count: {}\n",
|
||||
enabled.copper_layer_count
|
||||
));
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -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");
|
||||
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()));
|
||||
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!(
|
||||
"- 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<String
|
|||
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');
|
||||
|
||||
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("### 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<String
|
|||
}
|
||||
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!(
|
||||
"#### 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<String
|
|||
item.value.len()
|
||||
));
|
||||
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");
|
||||
}
|
||||
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) => {
|
||||
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(&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("## 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<Vec<String>, KiCadError> {
|
||||
let mut item_ids = Vec::new();
|
||||
let mut i = 0;
|
||||
|
|
|
|||
Loading…
Reference in New Issue