diff --git a/README.md b/README.md index eab03cb..66ade10 100644 --- a/README.md +++ b/README.md @@ -161,14 +161,14 @@ Legend: | `UpdateItems` | Implemented | `KiCadClient::update_items_raw`, `KiCadClient::update_items` | | `DeleteItems` | Implemented | `KiCadClient::delete_items_raw`, `KiCadClient::delete_items` | | `GetBoundingBox` | Implemented | `KiCadClient::get_item_bounding_boxes` | -| `GetSelection` | Implemented | `KiCadClient::get_selection_raw`, `KiCadClient::get_selection`, `KiCadClient::get_selection_summary`, `KiCadClient::get_selection_details` | -| `AddToSelection` | Implemented | `KiCadClient::add_to_selection_raw`, `KiCadClient::add_to_selection` | -| `RemoveFromSelection` | Implemented | `KiCadClient::remove_from_selection_raw`, `KiCadClient::remove_from_selection` | -| `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` | +| `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` (`SelectionMutationResult`) | +| `RemoveFromSelection` | Implemented | `KiCadClient::remove_from_selection_raw`, `KiCadClient::remove_from_selection` (`SelectionMutationResult`) | +| `ClearSelection` | Implemented | `KiCadClient::clear_selection_raw`, `KiCadClient::clear_selection` (`SelectionMutationResult`) | | `HitTest` | Implemented | `KiCadClient::hit_test_item` | | `GetTitleBlockInfo` | Implemented | `KiCadClient::get_title_block_info` | | `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` | ### Project manager diff --git a/docs/PCB_SELECTION_DEEP_DUMP.md b/docs/PCB_SELECTION_DEEP_DUMP.md new file mode 100644 index 0000000..9431faf --- /dev/null +++ b/docs/PCB_SELECTION_DEEP_DUMP.md @@ -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. diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 1ea22dd..0043fca 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -237,6 +237,8 @@ Show summary of current PCB selection by item type: 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: ```bash @@ -255,6 +257,8 @@ Add items to current selection: cargo run --features blocking --bin kicad-ipc-cli -- add-to-selection --id --id ``` +Output now comes from `SelectionMutationResult` (`summary` + decoded `items`). + Remove items from current selection: ```bash @@ -339,6 +343,8 @@ Dump selection text (KiCad s-expression): 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: ```bash diff --git a/docs/book/src/api-reference.md b/docs/book/src/api-reference.md index c5e1f38..a9c8d4e 100644 --- a/docs/book/src/api-reference.md +++ b/docs/book/src/api-reference.md @@ -10,3 +10,9 @@ Key items: - `KiCadClientBlocking` (`blocking` feature) - `KiCadError` - Typed models under `model::*` + +Selection API notes: + +- `get_selection_*` methods now take `type_codes: Vec` (`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`). diff --git a/examples/selection_deep_dump.rs b/examples/selection_deep_dump.rs new file mode 100644 index 0000000..b662761 --- /dev/null +++ b/examples/selection_deep_dump.rs @@ -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(label: &str, mut op: F) -> Result +where + F: FnMut() -> Result, +{ + 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 { + 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 { + 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 { + 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::>() + .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> { + 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 = 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 = selected_footprints + .iter() + .filter_map(|fp| fp.reference.clone()) + .collect(); + let selected_fp_ids: BTreeSet = 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> = 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::>().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 = [ + "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 = 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); +} diff --git a/src/blocking.rs b/src/blocking.rs index d826d9a..a527294 100644 --- a/src/blocking.rs +++ b/src/blocking.rs @@ -430,16 +430,16 @@ impl KiCadClientBlocking { fn set_visible_layers(&self, layer_ids: Vec) -> Result<(), KiCadError>; fn get_board_origin(&self, kind: BoardOriginKind) -> Result; fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>; - fn get_selection_summary(&self) -> Result; - fn get_selection_raw(&self) -> Result, KiCadError>; - fn get_selection_details(&self) -> Result, KiCadError>; - fn get_selection(&self) -> Result, KiCadError>; + fn get_selection_summary(&self, type_codes: Vec) -> Result; + fn get_selection_raw(&self, type_codes: Vec) -> Result, KiCadError>; + fn get_selection_details(&self, type_codes: Vec) -> Result, KiCadError>; + fn get_selection(&self, type_codes: Vec) -> Result, KiCadError>; fn add_to_selection_raw(&self, item_ids: Vec) -> Result, KiCadError>; - fn add_to_selection(&self, item_ids: Vec) -> Result; + fn add_to_selection(&self, item_ids: Vec) -> Result; fn clear_selection_raw(&self) -> Result, KiCadError>; - fn clear_selection(&self) -> Result; + fn clear_selection(&self) -> Result; fn remove_from_selection_raw(&self, item_ids: Vec) -> Result, KiCadError>; - fn remove_from_selection(&self, item_ids: Vec) -> Result; + fn remove_from_selection(&self, item_ids: Vec) -> Result; fn get_pad_netlist(&self) -> Result, KiCadError>; fn get_vias_raw(&self) -> Result, KiCadError>; fn get_vias(&self) -> Result, KiCadError>; @@ -477,7 +477,7 @@ impl KiCadClientBlocking { fn revert_document_raw(&self) -> Result; fn revert_document(&self) -> Result<(), KiCadError>; fn get_board_as_string(&self) -> Result; - fn get_selection_as_string(&self) -> Result; + fn get_selection_as_string(&self) -> Result; fn get_items_by_id_raw(&self, item_ids: Vec) -> Result, KiCadError>; fn get_items_by_id_details(&self, item_ids: Vec) -> Result, KiCadError>; fn get_items_by_id(&self, item_ids: Vec) -> Result, KiCadError>; diff --git a/src/client.rs b/src/client.rs index 5542405..d81a1f6 100644 --- a/src/client.rs +++ b/src/client.rs @@ -5,25 +5,8 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; use crate::envelope; use crate::error::KiCadError; -use crate::model::board::{ - ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, - 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::model::board::*; +use crate::model::common::*; use crate::proto::kiapi::board as board_proto; use crate::proto::kiapi::board::commands as board_commands; use crate::proto::kiapi::board::types as board_types; @@ -1043,7 +1026,10 @@ impl KiCadClient { } /// Returns a compact summary of the current PCB selection. - pub async fn get_selection_summary(&self) -> Result { + pub async fn get_selection_summary( + &self, + type_codes: Vec, + ) -> Result { let document = self.current_board_document_proto().await?; let command = common_commands::GetSelection { header: Some(common_types::ItemHeader { @@ -1051,7 +1037,7 @@ impl KiCadClient { container: None, field_mask: None, }), - types: Vec::new(), + types: type_codes, }; let response = self @@ -1061,13 +1047,16 @@ impl KiCadClient { let payload: common_commands::SelectionResponse = 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, KiCadError> { + pub async fn get_selection_raw( + &self, + type_codes: Vec, + ) -> Result, KiCadError> { let command = common_commands::GetSelection { header: Some(self.current_board_item_header().await?), - types: Vec::new(), + types: type_codes, }; let response = self @@ -1080,14 +1069,17 @@ impl KiCadClient { Ok(payload.items) } - pub async fn get_selection_details(&self) -> Result, KiCadError> { - let items = self.get_selection_raw().await?; + pub async fn get_selection_details( + &self, + type_codes: Vec, + ) -> Result, KiCadError> { + let items = self.get_selection_raw(type_codes).await?; summarize_item_details(items) } /// Returns the current selection as decoded typed PCB items. - pub async fn get_selection(&self) -> Result, KiCadError> { - let items = self.get_selection_raw().await?; + pub async fn get_selection(&self, type_codes: Vec) -> Result, KiCadError> { + let items = self.get_selection_raw(type_codes).await?; decode_pcb_items(items) } @@ -1123,9 +1115,11 @@ impl KiCadClient { pub async fn add_to_selection( &self, item_ids: Vec, - ) -> Result { - let items = self.add_to_selection_raw(item_ids).await?; - Ok(summarize_selection(items)) + ) -> Result { + let raw_items = self.add_to_selection_raw(item_ids).await?; + 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, KiCadError> { @@ -1150,9 +1144,11 @@ impl KiCadClient { } } - pub async fn clear_selection(&self) -> Result { - let items = self.clear_selection_raw().await?; - Ok(summarize_selection(items)) + pub async fn clear_selection(&self) -> Result { + let raw_items = self.clear_selection_raw().await?; + let summary = summarize_selection(&raw_items); + let items = decode_pcb_items(raw_items)?; + Ok(SelectionMutationResult { items, summary }) } pub async fn remove_from_selection_raw( @@ -1187,9 +1183,11 @@ impl KiCadClient { pub async fn remove_from_selection( &self, item_ids: Vec, - ) -> Result { - let items = self.remove_from_selection_raw(item_ids).await?; - Ok(summarize_selection(items)) + ) -> Result { + let raw_items = self.remove_from_selection_raw(item_ids).await?; + 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, KiCadError> { @@ -1846,7 +1844,7 @@ impl KiCadClient { } /// Serializes current selection to KiCad's string format. - pub async fn get_selection_as_string(&self) -> Result { + pub async fn get_selection_as_string(&self) -> Result { let command = common_commands::SaveSelectionToString {}; let response = self @@ -1854,7 +1852,10 @@ impl KiCadClient { .await?; let payload: common_commands::SavedSelectionResponse = 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( @@ -2381,10 +2382,10 @@ fn map_merge_mode_to_proto(value: MapMergeMode) -> i32 { } } -fn summarize_selection(items: Vec) -> SelectionSummary { +fn summarize_selection(items: &[prost_types::Any]) -> SelectionSummary { let mut counts = BTreeMap::::new(); - for item in &items { + for item in items { let entry = counts.entry(item.type_url.clone()).or_insert(0); *entry += 1; } @@ -3062,10 +3063,71 @@ fn map_via_type(value: i32) -> PcbViaType { } } -fn map_via_layers(pad_stack: Option) -> Option { +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 { 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 { + 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.end_layer)), @@ -3075,12 +3137,151 @@ fn map_via_layers(pad_stack: Option) -> Option, +) -> Option { + 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 { + 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 { + 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, +) -> Option { + 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 { match board_types::PadType::try_from(value) { Ok(board_types::PadType::PtPth) => PcbPadType::Pth, @@ -3113,6 +3314,7 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { start_nm: track.start.map(map_vector2_nm), end_nm: track.end.map(map_vector2_nm), width_nm: map_optional_distance_nm(track.width), + locked: map_lock_state(track.locked), layer: layer_to_model(track.layer), net: map_optional_net(track.net), })); @@ -3126,6 +3328,7 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { mid_nm: arc.mid.map(map_vector2_nm), end_nm: arc.end.map(map_vector2_nm), width_nm: map_optional_distance_nm(arc.width), + locked: map_lock_state(arc.locked), layer: layer_to_model(arc.layer), net: map_optional_net(arc.net), })); @@ -3137,7 +3340,9 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { id: via.id.map(|id| id.value), position_nm: via.position.map(map_vector2_nm), 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), })); } @@ -3154,6 +3359,32 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { .and_then(|board_text| board_text.text.as_ref()) .map(|text| text.text.clone()) .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 .definition .as_ref() @@ -3165,6 +3396,27 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { .count() }) .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 { id: footprint.id.map(|id| id.value), @@ -3172,17 +3424,42 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { position_nm: footprint.position.map(map_vector2_nm), orientation_deg: footprint.orientation.map(|angle| angle.value_degrees), 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, })); } if item.type_url == envelope::type_url("kiapi.board.types.Pad") { let pad = decode_any::(&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 { id: pad.id.map(|id| id.value), + locked: map_lock_state(pad.locked), number: pad.number, pad_type: map_pad_type(pad.r#type), 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), })); } @@ -3192,35 +3469,97 @@ fn decode_pcb_item(item: prost_types::Any) -> Result { &item, "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 .as_ref() - .and_then(|graphic| graphic.geometry.as_ref()) - .map(|value| format!("{value:?}")); + .and_then(|graphic| graphic.attributes.as_ref()) + .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 { id: shape.id.map(|id| id.value), layer: layer_to_model(shape.layer), + locked: map_lock_state(shape.locked), net: map_optional_net(shape.net), geometry_kind, + geometry, + stroke_width_nm, + stroke_style, + fill_type, })); } if item.type_url == envelope::type_url("kiapi.board.types.BoardText") { let text = decode_any::(&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 { id: text.id.map(|id| id.value), 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") { let textbox = decode_any::(&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 { id: textbox.id.map(|id| id.value), 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 { if item.type_url == envelope::type_url("kiapi.board.types.Zone") { let zone = decode_any::(&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::>(); + let layers = zone + .layers + .iter() + .copied() + .map(layer_to_model) + .collect::>(); + return Ok(PcbItem::Zone(PcbZone { id: zone.id.map(|id| id.value), name: zone.name, zone_type: map_zone_type(zone.r#type), + layers, layer_count: zone.layers.len(), + priority: zone.priority, + locked: map_lock_state(zone.locked), filled: zone.filled, 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") { let dimension = decode_any::(&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 { id: dimension.id.map(|id| id.value), layer: layer_to_model(dimension.layer), + locked: map_lock_state(dimension.locked), 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 { id: group.id.map(|id| id.value), name: group.name, 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) .map(|value| value.as_str_name().to_string()) .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 .as_ref() .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.type_url_counts.len(), 2); assert_eq!(summary.type_url_counts[0].count, 2); @@ -4340,6 +4766,35 @@ mod tests { 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] fn decode_pcb_item_maps_via_layers() { 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")); } + #[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] fn pad_netlist_from_footprint_items_extracts_pad_entries() { let pad = crate::proto::kiapi::board::types::Pad { diff --git a/src/lib.rs b/src/lib.rs index 357995d..78a46e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,18 +90,20 @@ pub use crate::model::board::{ ArcStartMidEndNm, BoardEditorAppearanceSettings, BoardEnabledLayers, BoardFlipMode, 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, + ColorRgba, DrcSeverity, GraphicsDefaults, InactiveLayerDisplayMode, ItemLockState, + NetClassBoardSettings, NetClassForNetEntry, NetClassInfo, NetClassType, NetColorDisplayMode, + PadNetEntry, PadShapeAsPolygonEntry, PadstackPresenceEntry, PadstackPresenceState, PcbArc, + PcbBoardGraphicShape, PcbBoardText, PcbBoardTextBox, PcbDimension, PcbDimensionStyle, PcbField, + PcbFootprint, PcbFootprintSymbolLink, PcbGraphicShapeGeometry, PcbGroup, PcbItem, PcbPad, + PcbPadStack, PcbPadType, PcbPadstackDrill, PcbSymbolPinInfo, PcbTextAttributes, PcbTrack, + PcbUnknownItem, PcbVia, PcbViaLayers, PcbViaType, PcbZone, PcbZoneLayerProperty, PcbZoneType, + PolyLineNm, PolyLineNodeGeometryNm, PolygonWithHolesNm, RatsnestDisplayMode, Vector2Nm, }; pub use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, - SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, - TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, - TextVerticalAlignment, TitleBlockInfo, VersionInfo, + SelectionMutationResult, SelectionStringDump, SelectionSummary, SelectionTypeCount, + TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, + TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, + VersionInfo, }; diff --git a/src/model/board.rs b/src/model/board.rs index 20ee55d..4edc57b 100644 --- a/src/model/board.rs +++ b/src/model/board.rs @@ -386,12 +386,144 @@ pub enum PcbZoneType { 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, + pub shape: Option, + pub capped: Option, + pub filled: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PcbPadStack { + pub stack_type: Option, + pub layers: Vec, + pub drill: Option, + pub unconnected_layer_removal: Option, + 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, + pub tertiary_drill: Option, + 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, + pub no_connect: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PcbTextAttributes { + pub font_name: Option, + pub horizontal_alignment: Option, + pub vertical_alignment: Option, + pub stroke_width_nm: Option, + pub italic: bool, + pub bold: bool, + pub underlined: bool, + pub mirrored: bool, + pub multiline: bool, + pub keep_upright: bool, + pub size_nm: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PcbGraphicShapeGeometry { + Segment { + start_nm: Option, + end_nm: Option, + }, + Rectangle { + top_left_nm: Option, + bottom_right_nm: Option, + corner_radius_nm: Option, + }, + Arc { + start_nm: Option, + mid_nm: Option, + end_nm: Option, + }, + Circle { + center_nm: Option, + radius_point_nm: Option, + }, + Polygon { + polygon_count: usize, + }, + Bezier { + start_nm: Option, + control1_nm: Option, + control2_nm: Option, + end_nm: Option, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PcbZoneLayerProperty { + pub layer: BoardLayerInfo, + pub hatching_offset_nm: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum PcbDimensionStyle { + Aligned { + start_nm: Option, + end_nm: Option, + height_nm: Option, + extension_height_nm: Option, + }, + Orthogonal { + start_nm: Option, + end_nm: Option, + height_nm: Option, + extension_height_nm: Option, + alignment: Option, + }, + Radial { + center_nm: Option, + radius_point_nm: Option, + leader_length_nm: Option, + }, + Leader { + start_nm: Option, + end_nm: Option, + border_style: Option, + }, + Center { + center_nm: Option, + end_nm: Option, + }, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PcbFootprintSymbolLink { + pub has_symbol_path: bool, + pub sheet_name: Option, + pub sheet_filename: Option, + pub footprint_filters: Option, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct PcbTrack { pub id: Option, pub start_nm: Option, pub end_nm: Option, pub width_nm: Option, + pub locked: ItemLockState, pub layer: BoardLayerInfo, pub net: Option, } @@ -403,6 +535,7 @@ pub struct PcbArc { pub mid_nm: Option, pub end_nm: Option, pub width_nm: Option, + pub locked: ItemLockState, pub layer: BoardLayerInfo, pub net: Option, } @@ -412,7 +545,9 @@ pub struct PcbVia { pub id: Option, pub position_nm: Option, pub via_type: PcbViaType, + pub locked: ItemLockState, pub layers: Option, + pub pad_stack: Option, pub net: Option, } @@ -423,15 +558,30 @@ pub struct PcbFootprint { pub position_nm: Option, pub orientation_deg: Option, pub layer: BoardLayerInfo, + pub locked: ItemLockState, + pub value: Option, + pub datasheet: Option, + pub description: Option, + pub has_attributes: bool, + pub has_overrides: bool, + pub has_definition: bool, + pub definition_item_count: usize, + pub symbol_link: Option, pub pad_count: usize, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct PcbPad { pub id: Option, + pub locked: ItemLockState, pub number: String, pub pad_type: PcbPadType, pub position_nm: Option, + pub pad_stack: Option, + pub copper_clearance_override_nm: Option, + pub pad_to_die_length_nm: Option, + pub pad_to_die_delay_as: Option, + pub symbol_pin: Option, pub net: Option, } @@ -439,8 +589,13 @@ pub struct PcbPad { pub struct PcbBoardGraphicShape { pub id: Option, pub layer: BoardLayerInfo, + pub locked: ItemLockState, pub net: Option, pub geometry_kind: Option, + pub geometry: Option, + pub stroke_width_nm: Option, + pub stroke_style: Option, + pub fill_type: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -448,6 +603,11 @@ pub struct PcbBoardText { pub id: Option, pub layer: BoardLayerInfo, pub text: Option, + pub position_nm: Option, + pub hyperlink: Option, + pub attributes: Option, + pub knockout: bool, + pub locked: ItemLockState, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -455,6 +615,10 @@ pub struct PcbBoardTextBox { pub id: Option, pub layer: BoardLayerInfo, pub text: Option, + pub top_left_nm: Option, + pub bottom_right_nm: Option, + pub attributes: Option, + pub locked: ItemLockState, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -469,17 +633,42 @@ pub struct PcbZone { pub id: Option, pub name: String, pub zone_type: PcbZoneType, + pub layers: Vec, pub layer_count: usize, + pub priority: u32, + pub locked: ItemLockState, pub filled: bool, pub polygon_count: usize, + pub outline_polygon_count: usize, + pub has_copper_settings: bool, + pub has_rule_area_settings: bool, + pub border_style: Option, + pub border_pitch_nm: Option, + pub layer_properties: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] pub struct PcbDimension { pub id: Option, pub layer: BoardLayerInfo, + pub locked: ItemLockState, pub text: Option, pub style_kind: Option, + pub style: Option, + pub override_text_enabled: bool, + pub override_text: Option, + pub prefix: Option, + pub suffix: Option, + pub unit: Option, + pub unit_format: Option, + pub arrow_direction: Option, + pub precision: Option, + pub suppress_trailing_zeroes: bool, + pub line_thickness_nm: Option, + pub arrow_length_nm: Option, + pub extension_offset_nm: Option, + pub text_position: Option, + pub keep_text_aligned: bool, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -487,6 +676,7 @@ pub struct PcbGroup { pub id: Option, pub name: String, pub item_count: usize, + pub item_ids: Vec, } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/src/model/common.rs b/src/model/common.rs index efc38d9..3fd1a4c 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -1,7 +1,7 @@ use std::path::PathBuf; 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; #[derive(Clone, Debug, Eq, PartialEq)] @@ -209,6 +209,24 @@ pub struct SelectionItemDetail { 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, + /// 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, + /// Compact composition summary for the same selection state. + pub summary: SelectionSummary, +} + #[derive(Clone, Debug, Eq, PartialEq)] /// Opaque commit session identifier returned by `begin_commit`. pub struct CommitSession { diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index 303e8fc..18f3d48 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -594,32 +594,32 @@ fn run() -> Result<(), KiCadError> { } } Command::AddToSelection { item_ids } => { - let summary = client.add_to_selection(item_ids)?; - println!("selection_total={}", summary.total_items); - for entry in summary.type_url_counts { + let result = client.add_to_selection(item_ids)?; + println!("selection_total={}", result.summary.total_items); + for entry in result.summary.type_url_counts { println!("type_url={} count={}", entry.type_url, entry.count); } } Command::RemoveFromSelection { item_ids } => { - let summary = client.remove_from_selection(item_ids)?; - println!("selection_total={}", summary.total_items); - for entry in summary.type_url_counts { + let result = client.remove_from_selection(item_ids)?; + println!("selection_total={}", result.summary.total_items); + for entry in result.summary.type_url_counts { println!("type_url={} count={}", entry.type_url, entry.count); } } Command::ClearSelection => { - let summary = client.clear_selection()?; - println!("selection_total={}", summary.total_items); + let result = client.clear_selection()?; + println!("selection_total={}", result.summary.total_items); } Command::SelectionSummary => { - let summary = client.get_selection_summary()?; + let summary = client.get_selection_summary(Vec::new())?; println!("selection_total={}", summary.total_items); for entry in summary.type_url_counts { println!("type_url={} count={}", entry.type_url, entry.count); } } Command::SelectionDetails => { - let details = client.get_selection_details()?; + let details = client.get_selection_details(Vec::new())?; println!("selection_total={}", details.len()); for (index, item) in details.iter().enumerate() { println!( @@ -629,7 +629,7 @@ fn run() -> Result<(), KiCadError> { } } Command::SelectionRaw => { - let items = client.get_selection_raw()?; + let items = client.get_selection_raw(Vec::new())?; println!("selection_total={}", items.len()); for (index, item) in items.iter().enumerate() { println!( @@ -851,8 +851,12 @@ fn run() -> Result<(), KiCadError> { println!("{content}"); } Command::SelectionAsString => { - let content = client.get_selection_as_string()?; - println!("{content}"); + let selection = client.get_selection_as_string()?; + println!("selection_id_count={}", selection.ids.len()); + for id in selection.ids { + println!("id={id}"); + } + println!("{}", selection.contents); } Command::Stackup => { let stackup = client.get_board_stackup()?; @@ -2140,7 +2144,7 @@ COMMANDS: Check padstack shape presence matrix across layers title-block Show title block fields 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 update-stackup Round-trip current stackup through UpdateBoardStackup graphics-defaults Show typed graphics defaults