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:
parent
e1c83bf561
commit
b32eb7fa44
10
README.md
10
README.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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`).
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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>;
|
||||||
|
|
|
||||||
666
src/client.rs
666
src/client.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
22
src/lib.rs
22
src/lib.rs
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue