fix: reduce selection API lossiness in existing public methods (#19)

* fix(selection): preserve selection payload fidelity in existing APIs

* docs(selection): update deep-dump example for revised selection signatures

* docs(selection): document revised selection signatures and return models
This commit is contained in:
Milind Sharma 2026-03-06 00:28:48 +08:00 committed by GitHub
parent e1c83bf561
commit b32eb7fa44
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1591 additions and 91 deletions

View File

@ -161,14 +161,14 @@ Legend:
| `UpdateItems` | Implemented | `KiCadClient::update_items_raw`, `KiCadClient::update_items` | | `UpdateItems` | Implemented | `KiCadClient::update_items_raw`, `KiCadClient::update_items` |
| `DeleteItems` | Implemented | `KiCadClient::delete_items_raw`, `KiCadClient::delete_items` | | `DeleteItems` | Implemented | `KiCadClient::delete_items_raw`, `KiCadClient::delete_items` |
| `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` |
| `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | | `GetSelection` | Implemented | `KiCadClient::get_selection_raw(type_codes)`, `KiCadClient::get_selection(type_codes)`, `KiCadClient::get_selection_summary(type_codes)`, `KiCadClient::get_selection_details(type_codes)` |
| `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` | | `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` (`SelectionMutationResult`) |
| `RemoveFromSelection` | Implemented | `KiCadClient::remove_from_selection_raw`, `KiCadClient::remove_from_selection` | | `RemoveFromSelection` | Implemented | `KiCadClient::remove_from_selection_raw`, `KiCadClient::remove_from_selection` (`SelectionMutationResult`) |
| `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` | | `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` (`SelectionMutationResult`) |
| `HitTest` | Implemented | `KiCadClient::hit_test_item` | | `HitTest` | Implemented | `KiCadClient::hit_test_item` |
| `GetTitleBlockInfo` | Implemented | `KiCadClient::get_title_block_info` | | `GetTitleBlockInfo` | Implemented | `KiCadClient::get_title_block_info` |
| `SaveDocumentToString` | Implemented | `KiCadClient::get_board_as_string` | | `SaveDocumentToString` | Implemented | `KiCadClient::get_board_as_string` |
| `SaveSelectionToString` | Implemented | `KiCadClient::get_selection_as_string` | | `SaveSelectionToString` | Implemented | `KiCadClient::get_selection_as_string` (`SelectionStringDump { ids, contents }`) |
| `ParseAndCreateItemsFromString` | Implemented | `KiCadClient::parse_and_create_items_from_string_raw`, `KiCadClient::parse_and_create_items_from_string` | | `ParseAndCreateItemsFromString` | Implemented | `KiCadClient::parse_and_create_items_from_string_raw`, `KiCadClient::parse_and_create_items_from_string` |
### Project manager ### Project manager

View File

@ -0,0 +1,83 @@
# PCB Selection Deep Dump
How to extract maximum data for current PCB selection using `kicad-ipc-rs` bindings.
## Script
Use:
- `examples/selection_deep_dump.rs`
Run:
```bash
RUSTFLAGS='-Awarnings' cargo run -q --features blocking --example selection_deep_dump
```
## What It Collects
- Selection summary:
- total selected item count
- per-type counts (`type_url`)
- Decoded selected items (`get_selection(Vec::new())`)
- Raw selected payload metadata (`get_selection_raw(Vec::new())`)
- Human-readable selection rows (`get_selection_details(Vec::new())`)
- Item IDs for all selected items
- Item bounding boxes (`get_item_bounding_boxes`)
- Footprint-level fields:
- reference
- UUID
- position (nm)
- orientation (deg)
- layer
- pad count
- Designator/value pairs from selection text (`get_selection_as_string().contents`)
- Selection text dump item IDs (`get_selection_as_string().ids`)
- Pad-level net rows for selected footprints (`get_pad_netlist`)
- Net graph among selected references (`net -> refs`)
- Net name to net code mapping (`get_nets`)
## API Sequence (Bindings)
- `KiCadClientBlocking::connect`
- `ping`
- `get_version`
- `get_selection_summary(Vec::new())`
- `get_selection(Vec::new())`
- `get_selection_raw(Vec::new())`
- `get_selection_details(Vec::new())`
- `get_item_bounding_boxes`
- `get_selection_as_string`
- `get_pad_netlist`
- `get_nets`
- `get_items_by_net` (best path; may be unavailable)
- fallback: `get_items_by_type_codes` per type code + local net-name filter
## Route/Trace Discovery Logic
Primary:
- Query `get_items_by_net` with types:
- `KOT_PCB_TRACE`
- `KOT_PCB_VIA`
- `KOT_PCB_ARC`
- `KOT_PCB_ZONE`
- `KOT_PCB_PAD`
- `KOT_PCB_SHAPE`
Fallback when `GetItemsByNet` is unsupported:
- Query each type separately with `get_items_by_type_codes(vec![type_code])`
- Filter returned items locally by `item.net.name in selected_net_names`
## Known KiCad 10.0.0-rc1 Behavior Seen
- `kiapi.board.commands.GetItemsByNet` can return `AS_UNHANDLED`.
- Script handles this and continues with fallback scan.
- On current session, fallback returned pad-connected items; no extra tracks/vias/arcs were returned for selected net names.
## Notes
- Script is read-only.
- Retries are built in for transient API unhandled/timeouts.
- Output is verbose by design, intended for debugging and data mining.

View File

@ -237,6 +237,8 @@ Show summary of current PCB selection by item type:
cargo run --features blocking --bin kicad-ipc-cli -- selection-summary cargo run --features blocking --bin kicad-ipc-cli -- selection-summary
``` ```
Note: CLI uses `Vec::new()` for `type_codes` on `get_selection_summary`, meaning unfiltered selection.
Show parsed details for currently selected items: Show parsed details for currently selected items:
```bash ```bash
@ -255,6 +257,8 @@ Add items to current selection:
cargo run --features blocking --bin kicad-ipc-cli -- add-to-selection --id <uuid> --id <uuid> cargo run --features blocking --bin kicad-ipc-cli -- add-to-selection --id <uuid> --id <uuid>
``` ```
Output now comes from `SelectionMutationResult` (`summary` + decoded `items`).
Remove items from current selection: Remove items from current selection:
```bash ```bash
@ -339,6 +343,8 @@ Dump selection text (KiCad s-expression):
cargo run --features blocking --bin kicad-ipc-cli -- selection-as-string cargo run --features blocking --bin kicad-ipc-cli -- selection-as-string
``` ```
Output includes `selection_id_count`, one `id=` line per selected item, then the `contents` text.
Dump title block fields: Dump title block fields:
```bash ```bash

View File

@ -10,3 +10,9 @@ Key items:
- `KiCadClientBlocking` (`blocking` feature) - `KiCadClientBlocking` (`blocking` feature)
- `KiCadError` - `KiCadError`
- Typed models under `model::*` - Typed models under `model::*`
Selection API notes:
- `get_selection_*` methods now take `type_codes: Vec<i32>` (`Vec::new()` means no filter).
- `add_to_selection`, `remove_from_selection`, `clear_selection` return `SelectionMutationResult` (decoded items + summary).
- `get_selection_as_string` returns `SelectionStringDump` (`ids` + `contents`).

View File

@ -0,0 +1,631 @@
#[cfg(feature = "blocking")]
use std::collections::{BTreeMap, BTreeSet};
#[cfg(feature = "blocking")]
use std::thread::sleep;
#[cfg(feature = "blocking")]
use std::time::Duration;
#[cfg(feature = "blocking")]
use kicad_ipc_rs::{
KiCadClient, KiCadClientBlocking, KiCadError, PcbArc, PcbBoardGraphicShape, PcbBoardText,
PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, PcbGroup, PcbItem, PcbPad, PcbTrack,
PcbVia, PcbZone,
};
#[cfg(feature = "blocking")]
fn retry<T, F>(label: &str, mut op: F) -> Result<T, KiCadError>
where
F: FnMut() -> Result<T, KiCadError>,
{
let attempts = 6;
for attempt in 1..=attempts {
match op() {
Ok(value) => return Ok(value),
Err(err) => {
if attempt == attempts {
return Err(err);
}
eprintln!("warn: {label} attempt {attempt} failed: {err}");
sleep(Duration::from_millis(300));
}
}
}
unreachable!("attempt loop exits via return");
}
#[cfg(feature = "blocking")]
fn item_id(item: &PcbItem) -> Option<&str> {
match item {
PcbItem::Track(v) => v.id.as_deref(),
PcbItem::Arc(v) => v.id.as_deref(),
PcbItem::Via(v) => v.id.as_deref(),
PcbItem::Footprint(v) => v.id.as_deref(),
PcbItem::Pad(v) => v.id.as_deref(),
PcbItem::BoardGraphicShape(v) => v.id.as_deref(),
PcbItem::BoardText(v) => v.id.as_deref(),
PcbItem::BoardTextBox(v) => v.id.as_deref(),
PcbItem::Field(_) => None,
PcbItem::Zone(v) => v.id.as_deref(),
PcbItem::Dimension(v) => v.id.as_deref(),
PcbItem::Group(v) => v.id.as_deref(),
PcbItem::Unknown(_) => None,
}
}
#[cfg(feature = "blocking")]
fn pcb_type_code(name: &str) -> Option<i32> {
KiCadClient::pcb_object_type_codes()
.iter()
.find(|entry| entry.name == name)
.map(|entry| entry.code)
}
#[cfg(feature = "blocking")]
fn extract_property(line: &str, key: &str) -> Option<String> {
let marker = format!("(property \"{key}\" \"");
let start = line.find(&marker)?;
let rest = &line[start + marker.len()..];
let end = rest.find('"')?;
Some(rest[..end].to_string())
}
#[cfg(feature = "blocking")]
fn parse_ref_values_from_selection_sexpr(contents: &str) -> BTreeMap<String, String> {
let mut refs = Vec::new();
let mut vals = Vec::new();
for line in contents.lines() {
if let Some(reference) = extract_property(line, "Reference") {
refs.push(reference);
}
if let Some(value) = extract_property(line, "Value") {
vals.push(value);
}
}
let mut out = BTreeMap::new();
let limit = refs.len().min(vals.len());
for index in 0..limit {
out.insert(refs[index].clone(), vals[index].clone());
}
out
}
#[cfg(feature = "blocking")]
fn print_item(item: &PcbItem) {
match item {
PcbItem::Track(PcbTrack {
id,
start_nm,
end_nm,
width_nm,
layer,
net,
..
}) => {
println!(
" track id={} start={:?} end={:?} width_nm={:?} layer={} net={}",
id.as_deref().unwrap_or("-"),
start_nm.map(|v| (v.x_nm, v.y_nm)),
end_nm.map(|v| (v.x_nm, v.y_nm)),
width_nm,
layer.name,
net.as_ref().map(|v| v.name.as_str()).unwrap_or("-")
);
}
PcbItem::Arc(PcbArc {
id,
start_nm,
mid_nm,
end_nm,
width_nm,
layer,
net,
..
}) => {
println!(
" arc id={} start={:?} mid={:?} end={:?} width_nm={:?} layer={} net={}",
id.as_deref().unwrap_or("-"),
start_nm.map(|v| (v.x_nm, v.y_nm)),
mid_nm.map(|v| (v.x_nm, v.y_nm)),
end_nm.map(|v| (v.x_nm, v.y_nm)),
width_nm,
layer.name,
net.as_ref().map(|v| v.name.as_str()).unwrap_or("-")
);
}
PcbItem::Via(PcbVia {
id,
position_nm,
via_type,
layers,
net,
..
}) => {
let stack = layers
.as_ref()
.map(|l| {
l.padstack_layers
.iter()
.map(|x| x.name.clone())
.collect::<Vec<_>>()
.join(",")
})
.unwrap_or_else(|| "-".to_string());
println!(
" via id={} pos={:?} via_type={:?} layers={} net={}",
id.as_deref().unwrap_or("-"),
position_nm.map(|v| (v.x_nm, v.y_nm)),
via_type,
stack,
net.as_ref().map(|v| v.name.as_str()).unwrap_or("-")
);
}
PcbItem::Footprint(PcbFootprint {
id,
reference,
position_nm,
orientation_deg,
layer,
pad_count,
..
}) => {
println!(
" footprint id={} ref={} pos={:?} orientation_deg={:?} layer={} pad_count={}",
id.as_deref().unwrap_or("-"),
reference.as_deref().unwrap_or("-"),
position_nm.map(|v| (v.x_nm, v.y_nm)),
orientation_deg,
layer.name,
pad_count
);
}
PcbItem::Pad(PcbPad {
id,
number,
pad_type,
position_nm,
net,
..
}) => {
println!(
" pad id={} number={} pad_type={:?} pos={:?} net={}",
id.as_deref().unwrap_or("-"),
number,
pad_type,
position_nm.map(|v| (v.x_nm, v.y_nm)),
net.as_ref().map(|v| v.name.as_str()).unwrap_or("-")
);
}
PcbItem::BoardGraphicShape(PcbBoardGraphicShape {
id,
layer,
net,
geometry_kind,
..
}) => {
println!(
" shape id={} layer={} geometry={} net={}",
id.as_deref().unwrap_or("-"),
layer.name,
geometry_kind.as_deref().unwrap_or("-"),
net.as_ref().map(|v| v.name.as_str()).unwrap_or("-")
);
}
PcbItem::BoardText(PcbBoardText {
id, layer, text, ..
}) => {
println!(
" text id={} layer={} text={}",
id.as_deref().unwrap_or("-"),
layer.name,
text.as_deref().unwrap_or("-")
);
}
PcbItem::BoardTextBox(PcbBoardTextBox {
id, layer, text, ..
}) => {
println!(
" textbox id={} layer={} text={}",
id.as_deref().unwrap_or("-"),
layer.name,
text.as_deref().unwrap_or("-")
);
}
PcbItem::Field(PcbField {
name,
visible,
text,
}) => {
println!(
" field name={} visible={} text={}",
name,
visible,
text.as_deref().unwrap_or("-")
);
}
PcbItem::Zone(PcbZone {
id,
name,
zone_type,
layer_count,
filled,
polygon_count,
..
}) => {
println!(
" zone id={} name={} zone_type={:?} layers={} filled={} polygons={}",
id.as_deref().unwrap_or("-"),
name,
zone_type,
layer_count,
filled,
polygon_count
);
}
PcbItem::Dimension(PcbDimension {
id,
layer,
text,
style_kind,
..
}) => {
println!(
" dimension id={} layer={} style={} text={}",
id.as_deref().unwrap_or("-"),
layer.name,
style_kind.as_deref().unwrap_or("-"),
text.as_deref().unwrap_or("-")
);
}
PcbItem::Group(PcbGroup {
id,
name,
item_count,
..
}) => {
println!(
" group id={} name={} item_count={}",
id.as_deref().unwrap_or("-"),
name,
item_count
);
}
PcbItem::Unknown(v) => {
println!(" unknown type={} raw_len={}", v.type_url, v.raw_len);
}
}
}
#[cfg(feature = "blocking")]
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = KiCadClientBlocking::connect()?;
retry("ping", || client.ping())?;
let version = retry("get_version", || client.get_version())?;
println!("version={}", version.full_version);
let summary = retry("get_selection_summary", || {
client.get_selection_summary(Vec::new())
})?;
println!("selection_total={}", summary.total_items);
for count in summary.type_url_counts {
println!(
"selection_type type_url={} count={}",
count.type_url, count.count
);
}
if summary.total_items == 0 {
println!("selection is empty; nothing to inspect");
return Ok(());
}
let selected_items = retry("get_selection", || client.get_selection(Vec::new()))?;
let selected_details = retry("get_selection_details", || {
client.get_selection_details(Vec::new())
})?;
let selected_raw = retry("get_selection_raw", || client.get_selection_raw(Vec::new()))?;
println!("selected_items_decoded={}", selected_items.len());
for item in &selected_items {
print_item(item);
}
println!("selected_items_raw={}", selected_raw.len());
for (idx, item) in selected_raw.iter().enumerate() {
println!(
" raw[{idx}] type_url={} raw_len={}",
item.type_url,
item.value.len()
);
}
println!("selected_items_detail_rows={}", selected_details.len());
for (idx, row) in selected_details.iter().enumerate() {
println!(
" detail[{idx}] type_url={} raw_len={} detail={}",
row.type_url, row.raw_len, row.detail
);
}
let selected_ids: Vec<String> = selected_items
.iter()
.filter_map(item_id)
.map(str::to_string)
.collect();
println!("selected_ids={}", selected_ids.len());
for id in &selected_ids {
println!(" id={id}");
}
if !selected_ids.is_empty() {
let bboxes = retry("get_item_bounding_boxes", || {
client.get_item_bounding_boxes(selected_ids.clone(), true)
})?;
println!("item_bounding_boxes={}", bboxes.len());
for bbox in bboxes {
println!(
" bbox id={} x_nm={} y_nm={} width_nm={} height_nm={}",
bbox.item_id, bbox.x_nm, bbox.y_nm, bbox.width_nm, bbox.height_nm
);
}
}
let mut selected_footprints = Vec::new();
for item in &selected_items {
if let PcbItem::Footprint(fp) = item {
selected_footprints.push(fp.clone());
}
}
let selected_refs: BTreeSet<String> = selected_footprints
.iter()
.filter_map(|fp| fp.reference.clone())
.collect();
let selected_fp_ids: BTreeSet<String> = selected_footprints
.iter()
.filter_map(|fp| fp.id.clone())
.collect();
println!("selected_footprints={}", selected_footprints.len());
for fp in &selected_footprints {
println!(
" footprint ref={} id={} pos={:?} orientation_deg={:?} layer={} pad_count={}",
fp.reference.as_deref().unwrap_or("-"),
fp.id.as_deref().unwrap_or("-"),
fp.position_nm.map(|v| (v.x_nm, v.y_nm)),
fp.orientation_deg,
fp.layer.name,
fp.pad_count
);
}
match retry("get_selection_as_string", || {
client.get_selection_as_string()
}) {
Ok(selection_dump) => {
println!("selection_string_ids={}", selection_dump.ids.len());
let ref_values = parse_ref_values_from_selection_sexpr(&selection_dump.contents);
println!("ref_value_pairs={}", ref_values.len());
for (reference, value) in ref_values {
println!(" {reference} => {value}");
}
}
Err(err) => {
println!("ref_value_pairs unavailable: {err}");
}
}
let pad_netlist = retry("get_pad_netlist", || client.get_pad_netlist())?;
let mut selected_pad_rows = Vec::new();
for row in pad_netlist {
let by_ref = row
.footprint_reference
.as_ref()
.map(|r| selected_refs.contains(r))
.unwrap_or(false);
let by_id = row
.footprint_id
.as_ref()
.map(|id| selected_fp_ids.contains(id))
.unwrap_or(false);
if by_ref || by_id {
selected_pad_rows.push(row);
}
}
println!("selected_pad_rows={}", selected_pad_rows.len());
let mut selected_net_names = BTreeSet::new();
let mut net_to_selected_refs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
for row in &selected_pad_rows {
let reference = row
.footprint_reference
.as_deref()
.map(str::to_string)
.unwrap_or_else(|| "-".to_string());
let net = row
.net_name
.as_deref()
.map(str::to_string)
.unwrap_or_else(|| "-".to_string());
println!(
" pad ref={} fp_id={} pad_id={} pad_number={} net_code={:?} net_name={}",
reference,
row.footprint_id.as_deref().unwrap_or("-"),
row.pad_id.as_deref().unwrap_or("-"),
row.pad_number,
row.net_code,
net
);
if net != "-" {
selected_net_names.insert(net.clone());
if reference != "-" {
net_to_selected_refs
.entry(net)
.or_default()
.insert(reference);
}
}
}
println!("selected_net_names={}", selected_net_names.len());
for net in &selected_net_names {
println!(" net={net}");
}
println!("interconnections_among_selected_refs");
for (net, refs) in &net_to_selected_refs {
if refs.len() >= 2 {
println!(
" net={} refs={}",
net,
refs.iter().cloned().collect::<Vec<_>>().join(",")
);
}
}
let all_nets = retry("get_nets", || client.get_nets())?;
let mut name_to_code = BTreeMap::new();
for net in all_nets {
name_to_code.insert(net.name, net.code);
}
let mut selected_net_codes = Vec::new();
for name in &selected_net_names {
if let Some(code) = name_to_code.get(name) {
selected_net_codes.push(*code);
}
}
selected_net_codes.sort_unstable();
selected_net_codes.dedup();
println!("selected_net_codes={:?}", selected_net_codes);
let route_type_codes: Vec<i32> = [
"KOT_PCB_TRACE",
"KOT_PCB_VIA",
"KOT_PCB_ARC",
"KOT_PCB_ZONE",
"KOT_PCB_PAD",
"KOT_PCB_SHAPE",
]
.into_iter()
.filter_map(pcb_type_code)
.collect();
println!("route_type_codes={route_type_codes:?}");
if !selected_net_codes.is_empty() && !route_type_codes.is_empty() {
match retry("get_items_by_net", || {
client.get_items_by_net(route_type_codes.clone(), selected_net_codes.clone())
}) {
Ok(connected_items) => {
let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
for item in &connected_items {
let label = match item {
PcbItem::Track(_) => "track",
PcbItem::Via(_) => "via",
PcbItem::Arc(_) => "arc",
PcbItem::Zone(_) => "zone",
PcbItem::Pad(_) => "pad",
_ => "other",
};
*counts.entry(label).or_insert(0) += 1;
}
println!("connected_items_on_selected_nets={}", connected_items.len());
for (label, count) in counts {
println!(" connected_{label}s={count}");
}
for item in connected_items.iter().take(80) {
print_item(item);
}
}
Err(err) => {
println!("connected_items_on_selected_nets unavailable via GetItemsByNet: {err}");
println!("connected_items fallback: scan-by-type + local net-name filter");
let mut items = Vec::new();
for code in &route_type_codes {
match retry("get_items_by_type_code_fallback", || {
client.get_items_by_type_codes(vec![*code])
}) {
Ok(mut bucket) => items.append(&mut bucket),
Err(type_err) => {
println!(" fallback type_code={code} unavailable: {type_err}");
}
}
}
if items.is_empty() {
println!("connected_items fallback unavailable: no items from type scans");
} else {
let filtered: Vec<PcbItem> = items
.into_iter()
.filter(|item| match item {
PcbItem::Track(track) => track
.net
.as_ref()
.map(|n| selected_net_names.contains(&n.name))
.unwrap_or(false),
PcbItem::Via(via) => via
.net
.as_ref()
.map(|n| selected_net_names.contains(&n.name))
.unwrap_or(false),
PcbItem::Arc(arc) => arc
.net
.as_ref()
.map(|n| selected_net_names.contains(&n.name))
.unwrap_or(false),
PcbItem::Pad(pad) => pad
.net
.as_ref()
.map(|n| selected_net_names.contains(&n.name))
.unwrap_or(false),
PcbItem::BoardGraphicShape(shape) => shape
.net
.as_ref()
.map(|n| selected_net_names.contains(&n.name))
.unwrap_or(false),
_ => false,
})
.collect();
let mut counts: BTreeMap<&'static str, usize> = BTreeMap::new();
for item in &filtered {
let label = match item {
PcbItem::Track(_) => "track",
PcbItem::Via(_) => "via",
PcbItem::Arc(_) => "arc",
PcbItem::Pad(_) => "pad",
PcbItem::BoardGraphicShape(_) => "shape",
_ => "other",
};
*counts.entry(label).or_insert(0) += 1;
}
println!(
"connected_items_on_selected_nets_fallback={}",
filtered.len()
);
for (label, count) in counts {
println!(" connected_{label}s={count}");
}
for item in filtered.iter().take(120) {
print_item(item);
}
}
}
}
} else {
println!("connected_items_on_selected_nets unavailable: no resolvable net codes");
}
Ok(())
}
#[cfg(not(feature = "blocking"))]
fn main() {
eprintln!("run with --features blocking");
std::process::exit(1);
}

View File

@ -430,16 +430,16 @@ impl KiCadClientBlocking {
fn set_visible_layers(&self, layer_ids: Vec<i32>) -> Result<(), KiCadError>; fn set_visible_layers(&self, layer_ids: Vec<i32>) -> Result<(), KiCadError>;
fn get_board_origin(&self, kind: BoardOriginKind) -> Result<Vector2Nm, KiCadError>; fn get_board_origin(&self, kind: BoardOriginKind) -> Result<Vector2Nm, KiCadError>;
fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>; fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>;
fn get_selection_summary(&self) -> Result<SelectionSummary, KiCadError>; fn get_selection_summary(&self, type_codes: Vec<i32>) -> Result<SelectionSummary, KiCadError>;
fn get_selection_raw(&self) -> Result<Vec<Any>, KiCadError>; fn get_selection_raw(&self, type_codes: Vec<i32>) -> Result<Vec<Any>, KiCadError>;
fn get_selection_details(&self) -> Result<Vec<SelectionItemDetail>, KiCadError>; fn get_selection_details(&self, type_codes: Vec<i32>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_selection(&self) -> Result<Vec<PcbItem>, KiCadError>; fn get_selection(&self, type_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError>;
fn add_to_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>; fn add_to_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn add_to_selection(&self, item_ids: Vec<String>) -> Result<SelectionSummary, KiCadError>; fn add_to_selection(&self, item_ids: Vec<String>) -> Result<SelectionMutationResult, KiCadError>;
fn clear_selection_raw(&self) -> Result<Vec<Any>, KiCadError>; fn clear_selection_raw(&self) -> Result<Vec<Any>, KiCadError>;
fn clear_selection(&self) -> Result<SelectionSummary, KiCadError>; fn clear_selection(&self) -> Result<SelectionMutationResult, KiCadError>;
fn remove_from_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>; fn remove_from_selection_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn remove_from_selection(&self, item_ids: Vec<String>) -> Result<SelectionSummary, KiCadError>; fn remove_from_selection(&self, item_ids: Vec<String>) -> Result<SelectionMutationResult, KiCadError>;
fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError>; fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError>;
fn get_vias_raw(&self) -> Result<Vec<Any>, KiCadError>; fn get_vias_raw(&self) -> Result<Vec<Any>, KiCadError>;
fn get_vias(&self) -> Result<Vec<PcbVia>, KiCadError>; fn get_vias(&self) -> Result<Vec<PcbVia>, KiCadError>;
@ -477,7 +477,7 @@ impl KiCadClientBlocking {
fn revert_document_raw(&self) -> Result<Any, KiCadError>; fn revert_document_raw(&self) -> Result<Any, KiCadError>;
fn revert_document(&self) -> Result<(), KiCadError>; fn revert_document(&self) -> Result<(), KiCadError>;
fn get_board_as_string(&self) -> Result<String, KiCadError>; fn get_board_as_string(&self) -> Result<String, KiCadError>;
fn get_selection_as_string(&self) -> Result<String, KiCadError>; fn get_selection_as_string(&self) -> Result<SelectionStringDump, KiCadError>;
fn get_items_by_id_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>; fn get_items_by_id_raw(&self, item_ids: Vec<String>) -> Result<Vec<Any>, KiCadError>;
fn get_items_by_id_details(&self, item_ids: Vec<String>) -> Result<Vec<SelectionItemDetail>, KiCadError>; fn get_items_by_id_details(&self, item_ids: Vec<String>) -> Result<Vec<SelectionItemDetail>, KiCadError>;
fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError>; fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError>;

View File

@ -5,25 +5,8 @@ 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::*;
ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, use crate::model::common::*;
BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind,
BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType,
ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings,
NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, PadNetEntry,
PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc,
PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint,
PcbGroup, PcbItem, PcbPad, PcbPadType, PcbTrack, PcbUnknownItem, PcbVia, PcbViaLayers,
PcbViaType, PcbZone, PcbZoneType, PolyLineNm, PolyLineNodeGeometryNm, PolygonWithHolesNm,
RatsnestDisplayMode, Vector2Nm,
};
use crate::model::common::{
CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox,
ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, ProjectInfo, RunActionStatus,
SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry,
TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec,
TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo,
};
use crate::proto::kiapi::board as board_proto; use crate::proto::kiapi::board as board_proto;
use crate::proto::kiapi::board::commands as board_commands; use crate::proto::kiapi::board::commands as board_commands;
use crate::proto::kiapi::board::types as board_types; use crate::proto::kiapi::board::types as board_types;
@ -1043,7 +1026,10 @@ impl KiCadClient {
} }
/// Returns a compact summary of the current PCB selection. /// Returns a compact summary of the current PCB selection.
pub async fn get_selection_summary(&self) -> Result<SelectionSummary, KiCadError> { pub async fn get_selection_summary(
&self,
type_codes: Vec<i32>,
) -> Result<SelectionSummary, KiCadError> {
let document = self.current_board_document_proto().await?; let document = self.current_board_document_proto().await?;
let command = common_commands::GetSelection { let command = common_commands::GetSelection {
header: Some(common_types::ItemHeader { header: Some(common_types::ItemHeader {
@ -1051,7 +1037,7 @@ impl KiCadClient {
container: None, container: None,
field_mask: None, field_mask: None,
}), }),
types: Vec::new(), types: type_codes,
}; };
let response = self let response = self
@ -1061,13 +1047,16 @@ impl KiCadClient {
let payload: common_commands::SelectionResponse = let payload: common_commands::SelectionResponse =
envelope::unpack_any(&response, RES_SELECTION_RESPONSE)?; envelope::unpack_any(&response, RES_SELECTION_RESPONSE)?;
Ok(summarize_selection(payload.items)) Ok(summarize_selection(&payload.items))
} }
pub async fn get_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> { pub async fn get_selection_raw(
&self,
type_codes: Vec<i32>,
) -> Result<Vec<prost_types::Any>, KiCadError> {
let command = common_commands::GetSelection { let command = common_commands::GetSelection {
header: Some(self.current_board_item_header().await?), header: Some(self.current_board_item_header().await?),
types: Vec::new(), types: type_codes,
}; };
let response = self let response = self
@ -1080,14 +1069,17 @@ impl KiCadClient {
Ok(payload.items) Ok(payload.items)
} }
pub async fn get_selection_details(&self) -> Result<Vec<SelectionItemDetail>, KiCadError> { pub async fn get_selection_details(
let items = self.get_selection_raw().await?; &self,
type_codes: Vec<i32>,
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
let items = self.get_selection_raw(type_codes).await?;
summarize_item_details(items) summarize_item_details(items)
} }
/// Returns the current selection as decoded typed PCB items. /// Returns the current selection as decoded typed PCB items.
pub async fn get_selection(&self) -> Result<Vec<PcbItem>, KiCadError> { pub async fn get_selection(&self, type_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError> {
let items = self.get_selection_raw().await?; let items = self.get_selection_raw(type_codes).await?;
decode_pcb_items(items) decode_pcb_items(items)
} }
@ -1123,9 +1115,11 @@ impl KiCadClient {
pub async fn add_to_selection( pub async fn add_to_selection(
&self, &self,
item_ids: Vec<String>, item_ids: Vec<String>,
) -> Result<SelectionSummary, KiCadError> { ) -> Result<SelectionMutationResult, KiCadError> {
let items = self.add_to_selection_raw(item_ids).await?; let raw_items = self.add_to_selection_raw(item_ids).await?;
Ok(summarize_selection(items)) let summary = summarize_selection(&raw_items);
let items = decode_pcb_items(raw_items)?;
Ok(SelectionMutationResult { items, summary })
} }
pub async fn clear_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> { pub async fn clear_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
@ -1150,9 +1144,11 @@ impl KiCadClient {
} }
} }
pub async fn clear_selection(&self) -> Result<SelectionSummary, KiCadError> { pub async fn clear_selection(&self) -> Result<SelectionMutationResult, KiCadError> {
let items = self.clear_selection_raw().await?; let raw_items = self.clear_selection_raw().await?;
Ok(summarize_selection(items)) let summary = summarize_selection(&raw_items);
let items = decode_pcb_items(raw_items)?;
Ok(SelectionMutationResult { items, summary })
} }
pub async fn remove_from_selection_raw( pub async fn remove_from_selection_raw(
@ -1187,9 +1183,11 @@ impl KiCadClient {
pub async fn remove_from_selection( pub async fn remove_from_selection(
&self, &self,
item_ids: Vec<String>, item_ids: Vec<String>,
) -> Result<SelectionSummary, KiCadError> { ) -> Result<SelectionMutationResult, KiCadError> {
let items = self.remove_from_selection_raw(item_ids).await?; let raw_items = self.remove_from_selection_raw(item_ids).await?;
Ok(summarize_selection(items)) let summary = summarize_selection(&raw_items);
let items = decode_pcb_items(raw_items)?;
Ok(SelectionMutationResult { items, summary })
} }
pub async fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError> { pub async fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError> {
@ -1846,7 +1844,7 @@ impl KiCadClient {
} }
/// Serializes current selection to KiCad's string format. /// Serializes current selection to KiCad's string format.
pub async fn get_selection_as_string(&self) -> Result<String, KiCadError> { pub async fn get_selection_as_string(&self) -> Result<SelectionStringDump, KiCadError> {
let command = common_commands::SaveSelectionToString {}; let command = common_commands::SaveSelectionToString {};
let response = self let response = self
@ -1854,7 +1852,10 @@ impl KiCadClient {
.await?; .await?;
let payload: common_commands::SavedSelectionResponse = let payload: common_commands::SavedSelectionResponse =
envelope::unpack_any(&response, RES_SAVED_SELECTION_RESPONSE)?; envelope::unpack_any(&response, RES_SAVED_SELECTION_RESPONSE)?;
Ok(payload.contents) Ok(SelectionStringDump {
ids: payload.ids.into_iter().map(|id| id.value).collect(),
contents: payload.contents,
})
} }
pub async fn get_items_by_id_raw( pub async fn get_items_by_id_raw(
@ -2381,10 +2382,10 @@ fn map_merge_mode_to_proto(value: MapMergeMode) -> i32 {
} }
} }
fn summarize_selection(items: Vec<prost_types::Any>) -> SelectionSummary { fn summarize_selection(items: &[prost_types::Any]) -> SelectionSummary {
let mut counts = BTreeMap::<String, usize>::new(); let mut counts = BTreeMap::<String, usize>::new();
for item in &items { for item in items {
let entry = counts.entry(item.type_url.clone()).or_insert(0); let entry = counts.entry(item.type_url.clone()).or_insert(0);
*entry += 1; *entry += 1;
} }
@ -3062,10 +3063,71 @@ fn map_via_type(value: i32) -> PcbViaType {
} }
} }
fn map_via_layers(pad_stack: Option<board_types::PadStack>) -> Option<PcbViaLayers> { fn map_lock_state(value: i32) -> ItemLockState {
match common_types::LockedState::try_from(value) {
Ok(common_types::LockedState::LsUnlocked) => ItemLockState::Unlocked,
Ok(common_types::LockedState::LsLocked) => ItemLockState::Locked,
_ => ItemLockState::Unknown(value),
}
}
fn map_padstack_drill(drill: board_types::DrillProperties) -> PcbPadstackDrill {
let shape = board_types::DrillShape::try_from(drill.shape)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", drill.shape));
let capped = board_types::ViaDrillCappingMode::try_from(drill.capped)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", drill.capped));
let filled = board_types::ViaDrillFillingMode::try_from(drill.filled)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", drill.filled));
PcbPadstackDrill {
start_layer: layer_to_model(drill.start_layer),
end_layer: layer_to_model(drill.end_layer),
diameter_nm: drill.diameter.map(map_vector2_nm),
shape: Some(shape),
capped: Some(capped),
filled: Some(filled),
}
}
fn map_pad_stack(pad_stack: Option<&board_types::PadStack>) -> Option<PcbPadStack> {
let pad_stack = pad_stack?; let pad_stack = pad_stack?;
let (drill_start_layer, drill_end_layer) = if let Some(drill) = pad_stack.drill { let stack_type = board_types::PadStackType::try_from(pad_stack.r#type)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", pad_stack.r#type));
let unconnected_layer_removal =
board_types::UnconnectedLayerRemoval::try_from(pad_stack.unconnected_layer_removal)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", pad_stack.unconnected_layer_removal));
Some(PcbPadStack {
stack_type: Some(stack_type),
layers: pad_stack
.layers
.iter()
.copied()
.map(layer_to_model)
.collect(),
drill: pad_stack.drill.map(map_padstack_drill),
unconnected_layer_removal: Some(unconnected_layer_removal),
copper_layer_count: pad_stack.copper_layers.len(),
has_front_outer_layers: pad_stack.front_outer_layers.is_some(),
has_back_outer_layers: pad_stack.back_outer_layers.is_some(),
has_zone_settings: pad_stack.zone_settings.is_some(),
secondary_drill: pad_stack.secondary_drill.map(map_padstack_drill),
tertiary_drill: pad_stack.tertiary_drill.map(map_padstack_drill),
has_front_post_machining: pad_stack.front_post_machining.is_some(),
has_back_post_machining: pad_stack.back_post_machining.is_some(),
})
}
fn map_via_layers(pad_stack: Option<&board_types::PadStack>) -> Option<PcbViaLayers> {
let pad_stack = pad_stack?;
let (drill_start_layer, drill_end_layer) = if let Some(drill) = pad_stack.drill.as_ref() {
( (
Some(layer_to_model(drill.start_layer)), Some(layer_to_model(drill.start_layer)),
Some(layer_to_model(drill.end_layer)), Some(layer_to_model(drill.end_layer)),
@ -3075,12 +3137,151 @@ fn map_via_layers(pad_stack: Option<board_types::PadStack>) -> Option<PcbViaLaye
}; };
Some(PcbViaLayers { Some(PcbViaLayers {
padstack_layers: pad_stack.layers.into_iter().map(layer_to_model).collect(), padstack_layers: pad_stack
.layers
.iter()
.copied()
.map(layer_to_model)
.collect(),
drill_start_layer, drill_start_layer,
drill_end_layer, drill_end_layer,
}) })
} }
fn map_text_attributes(
attributes: Option<common_types::TextAttributes>,
) -> Option<PcbTextAttributes> {
let attributes = attributes?;
let font_name = (!attributes.font_name.is_empty()).then_some(attributes.font_name);
let horizontal_alignment =
common_types::HorizontalAlignment::try_from(attributes.horizontal_alignment)
.map(|value| value.as_str_name().to_string())
.ok();
let vertical_alignment =
common_types::VerticalAlignment::try_from(attributes.vertical_alignment)
.map(|value| value.as_str_name().to_string())
.ok();
Some(PcbTextAttributes {
font_name,
horizontal_alignment,
vertical_alignment,
stroke_width_nm: map_optional_distance_nm(attributes.stroke_width),
italic: attributes.italic,
bold: attributes.bold,
underlined: attributes.underlined,
mirrored: attributes.mirrored,
multiline: attributes.multiline,
keep_upright: attributes.keep_upright,
size_nm: attributes.size.map(map_vector2_nm),
})
}
fn map_graphic_shape_geometry(
shape: Option<&common_types::GraphicShape>,
) -> Option<PcbGraphicShapeGeometry> {
let geometry = shape?.geometry.as_ref()?;
match geometry {
common_types::graphic_shape::Geometry::Segment(segment) => {
Some(PcbGraphicShapeGeometry::Segment {
start_nm: segment.start.map(map_vector2_nm),
end_nm: segment.end.map(map_vector2_nm),
})
}
common_types::graphic_shape::Geometry::Rectangle(rect) => {
Some(PcbGraphicShapeGeometry::Rectangle {
top_left_nm: rect.top_left.map(map_vector2_nm),
bottom_right_nm: rect.bottom_right.map(map_vector2_nm),
corner_radius_nm: map_optional_distance_nm(rect.corner_radius),
})
}
common_types::graphic_shape::Geometry::Arc(arc) => Some(PcbGraphicShapeGeometry::Arc {
start_nm: arc.start.map(map_vector2_nm),
mid_nm: arc.mid.map(map_vector2_nm),
end_nm: arc.end.map(map_vector2_nm),
}),
common_types::graphic_shape::Geometry::Circle(circle) => {
Some(PcbGraphicShapeGeometry::Circle {
center_nm: circle.center.map(map_vector2_nm),
radius_point_nm: circle.radius_point.map(map_vector2_nm),
})
}
common_types::graphic_shape::Geometry::Polygon(polyset) => {
Some(PcbGraphicShapeGeometry::Polygon {
polygon_count: polyset.polygons.len(),
})
}
common_types::graphic_shape::Geometry::Bezier(bezier) => {
Some(PcbGraphicShapeGeometry::Bezier {
start_nm: bezier.start.map(map_vector2_nm),
control1_nm: bezier.control1.map(map_vector2_nm),
control2_nm: bezier.control2.map(map_vector2_nm),
end_nm: bezier.end.map(map_vector2_nm),
})
}
}
}
fn map_graphic_shape_kind(shape: Option<&common_types::GraphicShape>) -> Option<String> {
let geometry = shape?.geometry.as_ref()?;
Some(match geometry {
common_types::graphic_shape::Geometry::Segment(_) => "SEGMENT".to_string(),
common_types::graphic_shape::Geometry::Rectangle(_) => "RECTANGLE".to_string(),
common_types::graphic_shape::Geometry::Arc(_) => "ARC".to_string(),
common_types::graphic_shape::Geometry::Circle(_) => "CIRCLE".to_string(),
common_types::graphic_shape::Geometry::Polygon(_) => "POLYGON".to_string(),
common_types::graphic_shape::Geometry::Bezier(_) => "BEZIER".to_string(),
})
}
fn map_dimension_style(
style: Option<board_types::dimension::DimensionStyle>,
) -> Option<PcbDimensionStyle> {
let style = style?;
match style {
board_types::dimension::DimensionStyle::Aligned(aligned) => {
Some(PcbDimensionStyle::Aligned {
start_nm: aligned.start.map(map_vector2_nm),
end_nm: aligned.end.map(map_vector2_nm),
height_nm: map_optional_distance_nm(aligned.height),
extension_height_nm: map_optional_distance_nm(aligned.extension_height),
})
}
board_types::dimension::DimensionStyle::Orthogonal(orthogonal) => {
let alignment = common_types::AxisAlignment::try_from(orthogonal.alignment)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", orthogonal.alignment));
Some(PcbDimensionStyle::Orthogonal {
start_nm: orthogonal.start.map(map_vector2_nm),
end_nm: orthogonal.end.map(map_vector2_nm),
height_nm: map_optional_distance_nm(orthogonal.height),
extension_height_nm: map_optional_distance_nm(orthogonal.extension_height),
alignment: Some(alignment),
})
}
board_types::dimension::DimensionStyle::Radial(radial) => Some(PcbDimensionStyle::Radial {
center_nm: radial.center.map(map_vector2_nm),
radius_point_nm: radial.radius_point.map(map_vector2_nm),
leader_length_nm: map_optional_distance_nm(radial.leader_length),
}),
board_types::dimension::DimensionStyle::Leader(leader) => {
let border_style = board_types::DimensionTextBorderStyle::try_from(leader.border_style)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", leader.border_style));
Some(PcbDimensionStyle::Leader {
start_nm: leader.start.map(map_vector2_nm),
end_nm: leader.end.map(map_vector2_nm),
border_style: Some(border_style),
})
}
board_types::dimension::DimensionStyle::Center(center) => Some(PcbDimensionStyle::Center {
center_nm: center.center.map(map_vector2_nm),
end_nm: center.end.map(map_vector2_nm),
}),
}
}
fn map_pad_type(value: i32) -> PcbPadType { fn map_pad_type(value: i32) -> PcbPadType {
match board_types::PadType::try_from(value) { match board_types::PadType::try_from(value) {
Ok(board_types::PadType::PtPth) => PcbPadType::Pth, Ok(board_types::PadType::PtPth) => PcbPadType::Pth,
@ -3113,6 +3314,7 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
start_nm: track.start.map(map_vector2_nm), start_nm: track.start.map(map_vector2_nm),
end_nm: track.end.map(map_vector2_nm), end_nm: track.end.map(map_vector2_nm),
width_nm: map_optional_distance_nm(track.width), width_nm: map_optional_distance_nm(track.width),
locked: map_lock_state(track.locked),
layer: layer_to_model(track.layer), layer: layer_to_model(track.layer),
net: map_optional_net(track.net), net: map_optional_net(track.net),
})); }));
@ -3126,6 +3328,7 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
mid_nm: arc.mid.map(map_vector2_nm), mid_nm: arc.mid.map(map_vector2_nm),
end_nm: arc.end.map(map_vector2_nm), end_nm: arc.end.map(map_vector2_nm),
width_nm: map_optional_distance_nm(arc.width), width_nm: map_optional_distance_nm(arc.width),
locked: map_lock_state(arc.locked),
layer: layer_to_model(arc.layer), layer: layer_to_model(arc.layer),
net: map_optional_net(arc.net), net: map_optional_net(arc.net),
})); }));
@ -3137,7 +3340,9 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
id: via.id.map(|id| id.value), id: via.id.map(|id| id.value),
position_nm: via.position.map(map_vector2_nm), position_nm: via.position.map(map_vector2_nm),
via_type: map_via_type(via.r#type), via_type: map_via_type(via.r#type),
layers: map_via_layers(via.pad_stack), locked: map_lock_state(via.locked),
layers: map_via_layers(via.pad_stack.as_ref()),
pad_stack: map_pad_stack(via.pad_stack.as_ref()),
net: map_optional_net(via.net), net: map_optional_net(via.net),
})); }));
} }
@ -3154,6 +3359,32 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
.and_then(|board_text| board_text.text.as_ref()) .and_then(|board_text| board_text.text.as_ref())
.map(|text| text.text.clone()) .map(|text| text.text.clone())
.filter(|value| !value.is_empty()); .filter(|value| !value.is_empty());
let value = footprint
.value_field
.as_ref()
.and_then(|field| field.text.as_ref())
.and_then(|board_text| board_text.text.as_ref())
.map(|text| text.text.clone())
.filter(|value| !value.is_empty());
let datasheet = footprint
.datasheet_field
.as_ref()
.and_then(|field| field.text.as_ref())
.and_then(|board_text| board_text.text.as_ref())
.map(|text| text.text.clone())
.filter(|value| !value.is_empty());
let description = footprint
.description_field
.as_ref()
.and_then(|field| field.text.as_ref())
.and_then(|board_text| board_text.text.as_ref())
.map(|text| text.text.clone())
.filter(|value| !value.is_empty());
let definition_item_count = footprint
.definition
.as_ref()
.map(|definition| definition.items.len())
.unwrap_or(0);
let pad_count = footprint let pad_count = footprint
.definition .definition
.as_ref() .as_ref()
@ -3165,6 +3396,27 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
.count() .count()
}) })
.unwrap_or(0); .unwrap_or(0);
let symbol_sheet_name = (!footprint.symbol_sheet_name.is_empty())
.then_some(footprint.symbol_sheet_name.clone());
let symbol_sheet_filename = (!footprint.symbol_sheet_filename.is_empty())
.then_some(footprint.symbol_sheet_filename.clone());
let symbol_footprint_filters = (!footprint.symbol_footprint_filters.is_empty())
.then_some(footprint.symbol_footprint_filters.clone());
let has_symbol_path = footprint.symbol_path.is_some();
let symbol_link = if has_symbol_path
|| symbol_sheet_name.is_some()
|| symbol_sheet_filename.is_some()
|| symbol_footprint_filters.is_some()
{
Some(PcbFootprintSymbolLink {
has_symbol_path,
sheet_name: symbol_sheet_name,
sheet_filename: symbol_sheet_filename,
footprint_filters: symbol_footprint_filters,
})
} else {
None
};
return Ok(PcbItem::Footprint(PcbFootprint { return Ok(PcbItem::Footprint(PcbFootprint {
id: footprint.id.map(|id| id.value), id: footprint.id.map(|id| id.value),
@ -3172,17 +3424,42 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
position_nm: footprint.position.map(map_vector2_nm), position_nm: footprint.position.map(map_vector2_nm),
orientation_deg: footprint.orientation.map(|angle| angle.value_degrees), orientation_deg: footprint.orientation.map(|angle| angle.value_degrees),
layer: layer_to_model(footprint.layer), layer: layer_to_model(footprint.layer),
locked: map_lock_state(footprint.locked),
value,
datasheet,
description,
has_attributes: footprint.attributes.is_some(),
has_overrides: footprint.overrides.is_some(),
has_definition: footprint.definition.is_some(),
definition_item_count,
symbol_link,
pad_count, pad_count,
})); }));
} }
if item.type_url == envelope::type_url("kiapi.board.types.Pad") { if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
let pad = decode_any::<board_types::Pad>(&item, "kiapi.board.types.Pad")?; let pad = decode_any::<board_types::Pad>(&item, "kiapi.board.types.Pad")?;
let symbol_pin = pad.symbol_pin.map(|pin| {
let pin_type = common_types::ElectricalPinType::try_from(pin.r#type)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", pin.r#type));
PcbSymbolPinInfo {
name: pin.name,
pin_type: Some(pin_type),
no_connect: pin.no_connect,
}
});
return Ok(PcbItem::Pad(PcbPad { return Ok(PcbItem::Pad(PcbPad {
id: pad.id.map(|id| id.value), id: pad.id.map(|id| id.value),
locked: map_lock_state(pad.locked),
number: pad.number, number: pad.number,
pad_type: map_pad_type(pad.r#type), pad_type: map_pad_type(pad.r#type),
position_nm: pad.position.map(map_vector2_nm), position_nm: pad.position.map(map_vector2_nm),
pad_stack: map_pad_stack(pad.pad_stack.as_ref()),
copper_clearance_override_nm: map_optional_distance_nm(pad.copper_clearance_override),
pad_to_die_length_nm: map_optional_distance_nm(pad.pad_to_die_length),
pad_to_die_delay_as: pad.pad_to_die_delay.map(|value| value.value_as),
symbol_pin,
net: map_optional_net(pad.net), net: map_optional_net(pad.net),
})); }));
} }
@ -3192,35 +3469,97 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
&item, &item,
"kiapi.board.types.BoardGraphicShape", "kiapi.board.types.BoardGraphicShape",
)?; )?;
let geometry_kind = shape let geometry_kind = map_graphic_shape_kind(shape.shape.as_ref());
let geometry = map_graphic_shape_geometry(shape.shape.as_ref());
let stroke_width_nm = shape
.shape .shape
.as_ref() .as_ref()
.and_then(|graphic| graphic.geometry.as_ref()) .and_then(|graphic| graphic.attributes.as_ref())
.map(|value| format!("{value:?}")); .and_then(|attrs| attrs.stroke.as_ref())
.and_then(|stroke| stroke.width)
.map(|width| width.value_nm);
let stroke_style = shape
.shape
.as_ref()
.and_then(|graphic| graphic.attributes.as_ref())
.and_then(|attrs| attrs.stroke.as_ref())
.map(|stroke| {
common_types::StrokeLineStyle::try_from(stroke.style)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", stroke.style))
});
let fill_type = shape
.shape
.as_ref()
.and_then(|graphic| graphic.attributes.as_ref())
.and_then(|attrs| attrs.fill.as_ref())
.map(|fill| {
common_types::GraphicFillType::try_from(fill.fill_type)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", fill.fill_type))
});
return Ok(PcbItem::BoardGraphicShape(PcbBoardGraphicShape { return Ok(PcbItem::BoardGraphicShape(PcbBoardGraphicShape {
id: shape.id.map(|id| id.value), id: shape.id.map(|id| id.value),
layer: layer_to_model(shape.layer), layer: layer_to_model(shape.layer),
locked: map_lock_state(shape.locked),
net: map_optional_net(shape.net), net: map_optional_net(shape.net),
geometry_kind, geometry_kind,
geometry,
stroke_width_nm,
stroke_style,
fill_type,
})); }));
} }
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") { if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
let text = decode_any::<board_types::BoardText>(&item, "kiapi.board.types.BoardText")?; let text = decode_any::<board_types::BoardText>(&item, "kiapi.board.types.BoardText")?;
let (body, position_nm, hyperlink, attributes) = if let Some(value) = text.text {
let hyperlink = (!value.hyperlink.is_empty()).then_some(value.hyperlink.clone());
let body = (!value.text.is_empty()).then_some(value.text.clone());
(
body,
value.position.map(map_vector2_nm),
hyperlink,
map_text_attributes(value.attributes),
)
} else {
(None, None, None, None)
};
return Ok(PcbItem::BoardText(PcbBoardText { return Ok(PcbItem::BoardText(PcbBoardText {
id: text.id.map(|id| id.value), id: text.id.map(|id| id.value),
layer: layer_to_model(text.layer), layer: layer_to_model(text.layer),
text: text.text.map(|value| value.text), text: body,
position_nm,
hyperlink,
attributes,
knockout: text.knockout,
locked: map_lock_state(text.locked),
})); }));
} }
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") { if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
let textbox = let textbox =
decode_any::<board_types::BoardTextBox>(&item, "kiapi.board.types.BoardTextBox")?; decode_any::<board_types::BoardTextBox>(&item, "kiapi.board.types.BoardTextBox")?;
let (body, top_left_nm, bottom_right_nm, attributes) = if let Some(value) = textbox.textbox
{
(
(!value.text.is_empty()).then_some(value.text.clone()),
value.top_left.map(map_vector2_nm),
value.bottom_right.map(map_vector2_nm),
map_text_attributes(value.attributes),
)
} else {
(None, None, None, None)
};
return Ok(PcbItem::BoardTextBox(PcbBoardTextBox { return Ok(PcbItem::BoardTextBox(PcbBoardTextBox {
id: textbox.id.map(|id| id.value), id: textbox.id.map(|id| id.value),
layer: layer_to_model(textbox.layer), layer: layer_to_model(textbox.layer),
text: textbox.textbox.map(|value| value.text), text: body,
top_left_nm,
bottom_right_nm,
attributes,
locked: map_lock_state(textbox.locked),
})); }));
} }
@ -3239,23 +3578,109 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
if item.type_url == envelope::type_url("kiapi.board.types.Zone") { if item.type_url == envelope::type_url("kiapi.board.types.Zone") {
let zone = decode_any::<board_types::Zone>(&item, "kiapi.board.types.Zone")?; let zone = decode_any::<board_types::Zone>(&item, "kiapi.board.types.Zone")?;
let has_copper_settings = matches!(
zone.settings,
Some(board_types::zone::Settings::CopperSettings(_))
);
let has_rule_area_settings = matches!(
zone.settings,
Some(board_types::zone::Settings::RuleAreaSettings(_))
);
let border_style = zone.border.as_ref().map(|border| {
board_types::ZoneBorderStyle::try_from(border.style)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", border.style))
});
let border_pitch_nm = zone
.border
.as_ref()
.and_then(|border| map_optional_distance_nm(border.pitch));
let layer_properties = zone
.layer_properties
.iter()
.map(|entry| PcbZoneLayerProperty {
layer: layer_to_model(entry.layer),
hatching_offset_nm: entry.hatching_offset.map(map_vector2_nm),
})
.collect::<Vec<_>>();
let layers = zone
.layers
.iter()
.copied()
.map(layer_to_model)
.collect::<Vec<_>>();
return Ok(PcbItem::Zone(PcbZone { return Ok(PcbItem::Zone(PcbZone {
id: zone.id.map(|id| id.value), id: zone.id.map(|id| id.value),
name: zone.name, name: zone.name,
zone_type: map_zone_type(zone.r#type), zone_type: map_zone_type(zone.r#type),
layers,
layer_count: zone.layers.len(), layer_count: zone.layers.len(),
priority: zone.priority,
locked: map_lock_state(zone.locked),
filled: zone.filled, filled: zone.filled,
polygon_count: zone.filled_polygons.len(), polygon_count: zone.filled_polygons.len(),
outline_polygon_count: zone.outline.map_or(0, |outline| outline.polygons.len()),
has_copper_settings,
has_rule_area_settings,
border_style,
border_pitch_nm,
layer_properties,
})); }));
} }
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") { if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
let dimension = decode_any::<board_types::Dimension>(&item, "kiapi.board.types.Dimension")?; let dimension = decode_any::<board_types::Dimension>(&item, "kiapi.board.types.Dimension")?;
let style_kind = dimension.dimension_style.as_ref().map(|value| match value {
board_types::dimension::DimensionStyle::Aligned(_) => "ALIGNED".to_string(),
board_types::dimension::DimensionStyle::Orthogonal(_) => "ORTHOGONAL".to_string(),
board_types::dimension::DimensionStyle::Radial(_) => "RADIAL".to_string(),
board_types::dimension::DimensionStyle::Leader(_) => "LEADER".to_string(),
board_types::dimension::DimensionStyle::Center(_) => "CENTER".to_string(),
});
let style = map_dimension_style(dimension.dimension_style);
let override_text =
(!dimension.override_text.is_empty()).then_some(dimension.override_text);
let prefix = (!dimension.prefix.is_empty()).then_some(dimension.prefix);
let suffix = (!dimension.suffix.is_empty()).then_some(dimension.suffix);
let unit = board_types::DimensionUnit::try_from(dimension.unit)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", dimension.unit));
let unit_format = board_types::DimensionUnitFormat::try_from(dimension.unit_format)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", dimension.unit_format));
let arrow_direction =
board_types::DimensionArrowDirection::try_from(dimension.arrow_direction)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", dimension.arrow_direction));
let precision = board_types::DimensionPrecision::try_from(dimension.precision)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", dimension.precision));
let text_position = board_types::DimensionTextPosition::try_from(dimension.text_position)
.map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", dimension.text_position));
return Ok(PcbItem::Dimension(PcbDimension { return Ok(PcbItem::Dimension(PcbDimension {
id: dimension.id.map(|id| id.value), id: dimension.id.map(|id| id.value),
layer: layer_to_model(dimension.layer), layer: layer_to_model(dimension.layer),
locked: map_lock_state(dimension.locked),
text: dimension.text.map(|value| value.text), text: dimension.text.map(|value| value.text),
style_kind: dimension.dimension_style.map(|value| format!("{value:?}")), style_kind,
style,
override_text_enabled: dimension.override_text_enabled,
override_text,
prefix,
suffix,
unit: Some(unit),
unit_format: Some(unit_format),
arrow_direction: Some(arrow_direction),
precision: Some(precision),
suppress_trailing_zeroes: dimension.suppress_trailing_zeroes,
line_thickness_nm: map_optional_distance_nm(dimension.line_thickness),
arrow_length_nm: map_optional_distance_nm(dimension.arrow_length),
extension_offset_nm: map_optional_distance_nm(dimension.extension_offset),
text_position: Some(text_position),
keep_text_aligned: dimension.keep_text_aligned,
})); }));
} }
@ -3265,6 +3690,7 @@ fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
id: group.id.map(|id| id.value), id: group.id.map(|id| id.value),
name: group.name, name: group.name,
item_count: group.items.len(), item_count: group.items.len(),
item_ids: group.items.into_iter().map(|item| item.value).collect(),
})); }));
} }
@ -3458,7 +3884,7 @@ fn format_via_selection_detail(via: board_types::Via) -> String {
let via_type = board_types::ViaType::try_from(via.r#type) let via_type = board_types::ViaType::try_from(via.r#type)
.map(|value| value.as_str_name().to_string()) .map(|value| value.as_str_name().to_string())
.unwrap_or_else(|_| format!("UNKNOWN({})", via.r#type)); .unwrap_or_else(|_| format!("UNKNOWN({})", via.r#type));
let layers = map_via_layers(via.pad_stack); let layers = map_via_layers(via.pad_stack.as_ref());
let pad_layers = layers let pad_layers = layers
.as_ref() .as_ref()
.map(|row| format_layer_names(&row.padstack_layers)) .map(|row| format_layer_names(&row.padstack_layers))
@ -4297,7 +4723,7 @@ mod tests {
}, },
]; ];
let summary = summarize_selection(items); let summary = summarize_selection(&items);
assert_eq!(summary.total_items, 3); assert_eq!(summary.total_items, 3);
assert_eq!(summary.type_url_counts.len(), 2); assert_eq!(summary.type_url_counts.len(), 2);
assert_eq!(summary.type_url_counts[0].count, 2); assert_eq!(summary.type_url_counts[0].count, 2);
@ -4340,6 +4766,35 @@ mod tests {
assert!(detail.contains("net=12:GND")); assert!(detail.contains("net=12:GND"));
} }
#[test]
fn decode_pcb_item_maps_track_locked_state() {
let track = crate::proto::kiapi::board::types::Track {
id: Some(crate::proto::kiapi::common::types::Kiid {
value: "track-id".to_string(),
}),
start: None,
end: None,
width: None,
locked: crate::proto::kiapi::common::types::LockedState::LsLocked as i32,
layer: crate::proto::kiapi::board::types::BoardLayer::BlFCu as i32,
net: None,
};
let item = prost_types::Any {
type_url: super::envelope::type_url("kiapi.board.types.Track"),
value: track.encode_to_vec(),
};
let parsed = decode_pcb_item(item).expect("track payload should decode");
match parsed {
PcbItem::Track(track) => {
assert_eq!(track.id.as_deref(), Some("track-id"));
assert_eq!(track.locked, crate::model::board::ItemLockState::Locked);
}
other => panic!("expected track item, got {other:?}"),
}
}
#[test] #[test]
fn decode_pcb_item_maps_via_layers() { fn decode_pcb_item_maps_via_layers() {
let via = crate::proto::kiapi::board::types::Via { let via = crate::proto::kiapi::board::types::Via {
@ -4441,6 +4896,111 @@ mod tests {
assert!(detail.contains("drill_span=BL_F_Cu->BL_B_Cu")); assert!(detail.contains("drill_span=BL_F_Cu->BL_B_Cu"));
} }
#[test]
fn decode_pcb_item_maps_group_item_ids() {
let group = crate::proto::kiapi::board::types::Group {
id: Some(crate::proto::kiapi::common::types::Kiid {
value: "group-id".to_string(),
}),
name: "group-a".to_string(),
items: vec![
crate::proto::kiapi::common::types::Kiid {
value: "item-1".to_string(),
},
crate::proto::kiapi::common::types::Kiid {
value: "item-2".to_string(),
},
],
};
let item = prost_types::Any {
type_url: super::envelope::type_url("kiapi.board.types.Group"),
value: group.encode_to_vec(),
};
let parsed = decode_pcb_item(item).expect("group payload should decode");
match parsed {
PcbItem::Group(group) => {
assert_eq!(group.id.as_deref(), Some("group-id"));
assert_eq!(group.item_count, 2);
assert_eq!(
group.item_ids,
vec!["item-1".to_string(), "item-2".to_string()]
);
}
other => panic!("expected group item, got {other:?}"),
}
}
#[test]
fn decode_pcb_item_maps_board_text_attributes() {
let text = crate::proto::kiapi::board::types::BoardText {
id: Some(crate::proto::kiapi::common::types::Kiid {
value: "text-id".to_string(),
}),
text: Some(crate::proto::kiapi::common::types::Text {
position: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 123,
y_nm: 456,
}),
attributes: Some(crate::proto::kiapi::common::types::TextAttributes {
font_name: "KiCad Font".to_string(),
horizontal_alignment:
crate::proto::kiapi::common::types::HorizontalAlignment::HaCenter as i32,
vertical_alignment: crate::proto::kiapi::common::types::VerticalAlignment::VaTop
as i32,
stroke_width: Some(crate::proto::kiapi::common::types::Distance {
value_nm: 42,
}),
italic: true,
bold: false,
underlined: true,
mirrored: false,
multiline: true,
keep_upright: true,
size: Some(crate::proto::kiapi::common::types::Vector2 {
x_nm: 777,
y_nm: 888,
}),
..Default::default()
}),
text: "HELLO".to_string(),
hyperlink: "https://example.com".to_string(),
}),
layer: crate::proto::kiapi::board::types::BoardLayer::BlFSilkS as i32,
knockout: true,
locked: crate::proto::kiapi::common::types::LockedState::LsUnlocked as i32,
};
let item = prost_types::Any {
type_url: super::envelope::type_url("kiapi.board.types.BoardText"),
value: text.encode_to_vec(),
};
let parsed = decode_pcb_item(item).expect("board text payload should decode");
match parsed {
PcbItem::BoardText(text) => {
assert_eq!(text.id.as_deref(), Some("text-id"));
assert_eq!(text.text.as_deref(), Some("HELLO"));
assert_eq!(text.hyperlink.as_deref(), Some("https://example.com"));
assert_eq!(text.knockout, true);
let attributes = text.attributes.expect("text attributes should map");
assert_eq!(attributes.font_name.as_deref(), Some("KiCad Font"));
assert_eq!(
attributes.horizontal_alignment.as_deref(),
Some("HA_CENTER")
);
assert_eq!(attributes.vertical_alignment.as_deref(), Some("VA_TOP"));
assert_eq!(attributes.stroke_width_nm, Some(42));
assert_eq!(
attributes.size_nm.map(|v| (v.x_nm, v.y_nm)),
Some((777, 888))
);
}
other => panic!("expected board text item, got {other:?}"),
}
}
#[test] #[test]
fn pad_netlist_from_footprint_items_extracts_pad_entries() { fn pad_netlist_from_footprint_items_extracts_pad_entries() {
let pad = crate::proto::kiapi::board::types::Pad { let pad = crate::proto::kiapi::board::types::Pad {

View File

@ -90,18 +90,20 @@ pub use crate::model::board::{
ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode,
BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind, BoardLayerClass, BoardLayerGraphicsDefault, BoardLayerInfo, BoardNet, BoardOriginKind,
BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType, BoardStackup, BoardStackupDielectricProperties, BoardStackupLayer, BoardStackupLayerType,
ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, NetClassBoardSettings, ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, ItemLockState,
NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, PadNetEntry, NetClassBoardSettings, NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode,
PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc, PadNetEntry, PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc,
PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbField, PcbFootprint, PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbDimensionStyle, PcbField,
PcbGroup, PcbItem, PcbPad, PcbPadType, PcbTrack, PcbUnknownItem, PcbVia, PcbViaLayers, PcbFootprint, PcbFootprintSymbolLink, PcbGraphicShapeGeometry, PcbGroup, PcbItem, PcbPad,
PcbViaType, PcbZone, PcbZoneType, PolyLineNm, PolyLineNodeGeometryNm, PolygonWithHolesNm, PcbPadStack, PcbPadType, PcbPadstackDrill, PcbSymbolPinInfo, PcbTextAttributes, PcbTrack,
RatsnestDisplayMode, Vector2Nm, PcbUnknownItem, PcbVia, PcbViaLayers, PcbViaType, PcbZone, PcbZoneLayerProperty, PcbZoneType,
PolyLineNm, PolyLineNodeGeometryNm, PolygonWithHolesNm, RatsnestDisplayMode, Vector2Nm,
}; };
pub use crate::model::common::{ pub use crate::model::common::{
CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox,
ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail,
SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, SelectionMutationResult, SelectionStringDump, SelectionSummary, SelectionTypeCount,
TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment,
TextVerticalAlignment, TitleBlockInfo, VersionInfo, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo,
VersionInfo,
}; };

View File

@ -386,12 +386,144 @@ pub enum PcbZoneType {
Unknown(i32), Unknown(i32),
} }
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ItemLockState {
Unlocked,
Locked,
Unknown(i32),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbPadstackDrill {
pub start_layer: BoardLayerInfo,
pub end_layer: BoardLayerInfo,
pub diameter_nm: Option<Vector2Nm>,
pub shape: Option<String>,
pub capped: Option<String>,
pub filled: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbPadStack {
pub stack_type: Option<String>,
pub layers: Vec<BoardLayerInfo>,
pub drill: Option<PcbPadstackDrill>,
pub unconnected_layer_removal: Option<String>,
pub copper_layer_count: usize,
pub has_front_outer_layers: bool,
pub has_back_outer_layers: bool,
pub has_zone_settings: bool,
pub secondary_drill: Option<PcbPadstackDrill>,
pub tertiary_drill: Option<PcbPadstackDrill>,
pub has_front_post_machining: bool,
pub has_back_post_machining: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbSymbolPinInfo {
pub name: String,
pub pin_type: Option<String>,
pub no_connect: bool,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbTextAttributes {
pub font_name: Option<String>,
pub horizontal_alignment: Option<String>,
pub vertical_alignment: Option<String>,
pub stroke_width_nm: Option<i64>,
pub italic: bool,
pub bold: bool,
pub underlined: bool,
pub mirrored: bool,
pub multiline: bool,
pub keep_upright: bool,
pub size_nm: Option<Vector2Nm>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PcbGraphicShapeGeometry {
Segment {
start_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
},
Rectangle {
top_left_nm: Option<Vector2Nm>,
bottom_right_nm: Option<Vector2Nm>,
corner_radius_nm: Option<i64>,
},
Arc {
start_nm: Option<Vector2Nm>,
mid_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
},
Circle {
center_nm: Option<Vector2Nm>,
radius_point_nm: Option<Vector2Nm>,
},
Polygon {
polygon_count: usize,
},
Bezier {
start_nm: Option<Vector2Nm>,
control1_nm: Option<Vector2Nm>,
control2_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbZoneLayerProperty {
pub layer: BoardLayerInfo,
pub hatching_offset_nm: Option<Vector2Nm>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PcbDimensionStyle {
Aligned {
start_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
height_nm: Option<i64>,
extension_height_nm: Option<i64>,
},
Orthogonal {
start_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
height_nm: Option<i64>,
extension_height_nm: Option<i64>,
alignment: Option<String>,
},
Radial {
center_nm: Option<Vector2Nm>,
radius_point_nm: Option<Vector2Nm>,
leader_length_nm: Option<i64>,
},
Leader {
start_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
border_style: Option<String>,
},
Center {
center_nm: Option<Vector2Nm>,
end_nm: Option<Vector2Nm>,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbFootprintSymbolLink {
pub has_symbol_path: bool,
pub sheet_name: Option<String>,
pub sheet_filename: Option<String>,
pub footprint_filters: Option<String>,
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbTrack { pub struct PcbTrack {
pub id: Option<String>, pub id: Option<String>,
pub start_nm: Option<Vector2Nm>, pub start_nm: Option<Vector2Nm>,
pub end_nm: Option<Vector2Nm>, pub end_nm: Option<Vector2Nm>,
pub width_nm: Option<i64>, pub width_nm: Option<i64>,
pub locked: ItemLockState,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub net: Option<BoardNet>, pub net: Option<BoardNet>,
} }
@ -403,6 +535,7 @@ pub struct PcbArc {
pub mid_nm: Option<Vector2Nm>, pub mid_nm: Option<Vector2Nm>,
pub end_nm: Option<Vector2Nm>, pub end_nm: Option<Vector2Nm>,
pub width_nm: Option<i64>, pub width_nm: Option<i64>,
pub locked: ItemLockState,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub net: Option<BoardNet>, pub net: Option<BoardNet>,
} }
@ -412,7 +545,9 @@ pub struct PcbVia {
pub id: Option<String>, pub id: Option<String>,
pub position_nm: Option<Vector2Nm>, pub position_nm: Option<Vector2Nm>,
pub via_type: PcbViaType, pub via_type: PcbViaType,
pub locked: ItemLockState,
pub layers: Option<PcbViaLayers>, pub layers: Option<PcbViaLayers>,
pub pad_stack: Option<PcbPadStack>,
pub net: Option<BoardNet>, pub net: Option<BoardNet>,
} }
@ -423,15 +558,30 @@ pub struct PcbFootprint {
pub position_nm: Option<Vector2Nm>, pub position_nm: Option<Vector2Nm>,
pub orientation_deg: Option<f64>, pub orientation_deg: Option<f64>,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub locked: ItemLockState,
pub value: Option<String>,
pub datasheet: Option<String>,
pub description: Option<String>,
pub has_attributes: bool,
pub has_overrides: bool,
pub has_definition: bool,
pub definition_item_count: usize,
pub symbol_link: Option<PcbFootprintSymbolLink>,
pub pad_count: usize, pub pad_count: usize,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbPad { pub struct PcbPad {
pub id: Option<String>, pub id: Option<String>,
pub locked: ItemLockState,
pub number: String, pub number: String,
pub pad_type: PcbPadType, pub pad_type: PcbPadType,
pub position_nm: Option<Vector2Nm>, pub position_nm: Option<Vector2Nm>,
pub pad_stack: Option<PcbPadStack>,
pub copper_clearance_override_nm: Option<i64>,
pub pad_to_die_length_nm: Option<i64>,
pub pad_to_die_delay_as: Option<i64>,
pub symbol_pin: Option<PcbSymbolPinInfo>,
pub net: Option<BoardNet>, pub net: Option<BoardNet>,
} }
@ -439,8 +589,13 @@ pub struct PcbPad {
pub struct PcbBoardGraphicShape { pub struct PcbBoardGraphicShape {
pub id: Option<String>, pub id: Option<String>,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub locked: ItemLockState,
pub net: Option<BoardNet>, pub net: Option<BoardNet>,
pub geometry_kind: Option<String>, pub geometry_kind: Option<String>,
pub geometry: Option<PcbGraphicShapeGeometry>,
pub stroke_width_nm: Option<i64>,
pub stroke_style: Option<String>,
pub fill_type: Option<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -448,6 +603,11 @@ pub struct PcbBoardText {
pub id: Option<String>, pub id: Option<String>,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub text: Option<String>, pub text: Option<String>,
pub position_nm: Option<Vector2Nm>,
pub hyperlink: Option<String>,
pub attributes: Option<PcbTextAttributes>,
pub knockout: bool,
pub locked: ItemLockState,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -455,6 +615,10 @@ pub struct PcbBoardTextBox {
pub id: Option<String>, pub id: Option<String>,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub text: Option<String>, pub text: Option<String>,
pub top_left_nm: Option<Vector2Nm>,
pub bottom_right_nm: Option<Vector2Nm>,
pub attributes: Option<PcbTextAttributes>,
pub locked: ItemLockState,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -469,17 +633,42 @@ pub struct PcbZone {
pub id: Option<String>, pub id: Option<String>,
pub name: String, pub name: String,
pub zone_type: PcbZoneType, pub zone_type: PcbZoneType,
pub layers: Vec<BoardLayerInfo>,
pub layer_count: usize, pub layer_count: usize,
pub priority: u32,
pub locked: ItemLockState,
pub filled: bool, pub filled: bool,
pub polygon_count: usize, pub polygon_count: usize,
pub outline_polygon_count: usize,
pub has_copper_settings: bool,
pub has_rule_area_settings: bool,
pub border_style: Option<String>,
pub border_pitch_nm: Option<i64>,
pub layer_properties: Vec<PcbZoneLayerProperty>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub struct PcbDimension { pub struct PcbDimension {
pub id: Option<String>, pub id: Option<String>,
pub layer: BoardLayerInfo, pub layer: BoardLayerInfo,
pub locked: ItemLockState,
pub text: Option<String>, pub text: Option<String>,
pub style_kind: Option<String>, pub style_kind: Option<String>,
pub style: Option<PcbDimensionStyle>,
pub override_text_enabled: bool,
pub override_text: Option<String>,
pub prefix: Option<String>,
pub suffix: Option<String>,
pub unit: Option<String>,
pub unit_format: Option<String>,
pub arrow_direction: Option<String>,
pub precision: Option<String>,
pub suppress_trailing_zeroes: bool,
pub line_thickness_nm: Option<i64>,
pub arrow_length_nm: Option<i64>,
pub extension_offset_nm: Option<i64>,
pub text_position: Option<String>,
pub keep_text_aligned: bool,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -487,6 +676,7 @@ pub struct PcbGroup {
pub id: Option<String>, pub id: Option<String>,
pub name: String, pub name: String,
pub item_count: usize, pub item_count: usize,
pub item_ids: Vec<String>,
} }
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]

View File

@ -1,7 +1,7 @@
use std::path::PathBuf; use std::path::PathBuf;
use std::str::FromStr; use std::str::FromStr;
use crate::model::board::{ColorRgba, PolygonWithHolesNm, Vector2Nm}; use crate::model::board::{ColorRgba, PcbItem, PolygonWithHolesNm, Vector2Nm};
use crate::proto::kiapi::common::types as common_types; use crate::proto::kiapi::common::types as common_types;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
@ -209,6 +209,24 @@ pub struct SelectionItemDetail {
pub raw_len: usize, pub raw_len: usize,
} }
#[derive(Clone, Debug, Eq, PartialEq)]
/// Selection dump returned by `get_selection_as_string`.
pub struct SelectionStringDump {
/// Ordered ids included in the serialized selection payload.
pub ids: Vec<String>,
/// Selection serialized as KiCad s-expression text.
pub contents: String,
}
#[derive(Clone, Debug, PartialEq)]
/// Result of add/remove/clear selection mutations.
pub struct SelectionMutationResult {
/// Decoded selected items after mutation.
pub items: Vec<PcbItem>,
/// Compact composition summary for the same selection state.
pub summary: SelectionSummary,
}
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
/// Opaque commit session identifier returned by `begin_commit`. /// Opaque commit session identifier returned by `begin_commit`.
pub struct CommitSession { pub struct CommitSession {

View File

@ -594,32 +594,32 @@ fn run() -> Result<(), KiCadError> {
} }
} }
Command::AddToSelection { item_ids } => { Command::AddToSelection { item_ids } => {
let summary = client.add_to_selection(item_ids)?; let result = client.add_to_selection(item_ids)?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", result.summary.total_items);
for entry in summary.type_url_counts { for entry in result.summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count); println!("type_url={} count={}", entry.type_url, entry.count);
} }
} }
Command::RemoveFromSelection { item_ids } => { Command::RemoveFromSelection { item_ids } => {
let summary = client.remove_from_selection(item_ids)?; let result = client.remove_from_selection(item_ids)?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", result.summary.total_items);
for entry in summary.type_url_counts { for entry in result.summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count); println!("type_url={} count={}", entry.type_url, entry.count);
} }
} }
Command::ClearSelection => { Command::ClearSelection => {
let summary = client.clear_selection()?; let result = client.clear_selection()?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", result.summary.total_items);
} }
Command::SelectionSummary => { Command::SelectionSummary => {
let summary = client.get_selection_summary()?; let summary = client.get_selection_summary(Vec::new())?;
println!("selection_total={}", summary.total_items); println!("selection_total={}", summary.total_items);
for entry in summary.type_url_counts { for entry in summary.type_url_counts {
println!("type_url={} count={}", entry.type_url, entry.count); println!("type_url={} count={}", entry.type_url, entry.count);
} }
} }
Command::SelectionDetails => { Command::SelectionDetails => {
let details = client.get_selection_details()?; let details = client.get_selection_details(Vec::new())?;
println!("selection_total={}", details.len()); println!("selection_total={}", details.len());
for (index, item) in details.iter().enumerate() { for (index, item) in details.iter().enumerate() {
println!( println!(
@ -629,7 +629,7 @@ fn run() -> Result<(), KiCadError> {
} }
} }
Command::SelectionRaw => { Command::SelectionRaw => {
let items = client.get_selection_raw()?; let items = client.get_selection_raw(Vec::new())?;
println!("selection_total={}", items.len()); println!("selection_total={}", items.len());
for (index, item) in items.iter().enumerate() { for (index, item) in items.iter().enumerate() {
println!( println!(
@ -851,8 +851,12 @@ fn run() -> Result<(), KiCadError> {
println!("{content}"); println!("{content}");
} }
Command::SelectionAsString => { Command::SelectionAsString => {
let content = client.get_selection_as_string()?; let selection = client.get_selection_as_string()?;
println!("{content}"); println!("selection_id_count={}", selection.ids.len());
for id in selection.ids {
println!("id={id}");
}
println!("{}", selection.contents);
} }
Command::Stackup => { Command::Stackup => {
let stackup = client.get_board_stackup()?; let stackup = client.get_board_stackup()?;
@ -2140,7 +2144,7 @@ COMMANDS:
Check padstack shape presence matrix across layers Check padstack shape presence matrix across layers
title-block Show title block fields title-block Show title block fields
board-as-string Dump board as KiCad s-expression text board-as-string Dump board as KiCad s-expression text
selection-as-string Dump current selection as KiCad s-expression text selection-as-string Dump current selection IDs + KiCad s-expression text
stackup Show typed board stackup stackup Show typed board stackup
update-stackup Round-trip current stackup through UpdateBoardStackup update-stackup Round-trip current stackup through UpdateBoardStackup
graphics-defaults Show typed graphics defaults graphics-defaults Show typed graphics defaults