refactor: modularize client API and finalize v10 assessment follow-ups (#25)
* feat: bump vendored KiCad protos to v10.0.0 * test: add protocol contract tests for board layer name * docs: overhaul README and guide site - Rewrite README with punchy opening, realistic examples, and cleaner structure - Update status to Beta and version numbers to 0.4.1 - Remove redundant sections (roadmap, future work, guide site link) - Simplify API matrix by removing redundant Status column - Add CONTRIBUTING.md header with welcoming message - Expand mdBook examples with real-world patterns: - PCB analysis (unconnected nets, footprints) - Automation (text variables, test points) - CI/CD integration patterns - Net class validation - Selection manipulation - Update mdBook intro with comparison table and clearer goals - Update quickstart version numbers - Suppress missing_docs warnings for internal modules (commands, envelope, transport) - Format code with cargo fmt * docs: complete library assessment report with verified findings - Corrected baseline metrics (5448 LOC client.rs, 7903 non-generated total, 12766 overall) - Added full non-generated source tree with LOC breakdown - Expanded anti-pattern scan from 3 to 6 findings (AP-4 through AP-6) - Added verified clean signals (zero production unwrap/expect/panic) - Added transport architecture, feature flag, and model cross-dependency analysis - Proposed concrete client.rs domain split into 8 modules - Identified 5 new documentation issues (DR-2 through DR-6) - Resolved DR-1 (version drift already fixed) - Expanded risk register from 5 to 9 entries - Updated prioritized action plan with corrected priorities * fix: complete P0 action items from library assessment - Fix clone_on_copy in client.rs map_text_shape (AP-1) - Add clippy::enum_variant_names allow for generated proto code (AP-2) - Fix bool_assert_comparison patterns in test assertions (AP-3) - Fix broken README anchor in validation.md (DR-2) - Remove docs/book/src/https: filesystem artifact (DR-3) * refactor: split monolithic client.rs into domain modules Split src/client.rs (5448 LOC) into src/client/ directory with 11 modules: - mod.rs: core structs, builder, constants, send_command - common.rs: ping, version, paths, documents, text vars, text geometry - board.rs: nets, layers, origin, stackup, graphics, appearance, DRC - selection.rs: get/add/remove/clear selection - items.rs: CRUD, get by type/net/class, commit workflow - document.rs: title block, save, revert, string serialization - geometry.rs: bounding boxes, hit test, pad polygons, padstack, zones - mappers.rs: all proto-to-model and model-to-proto conversions - decode.rs: PCB item type decoding - format.rs: selection detail formatting, debug utilities - tests.rs: all unit tests No public API changes. All existing tests pass. Updated blocking parity test to scan split module files. * refactor: add rpc! dispatch macro to reduce RPC boilerplate Introduce rpc! macro in client/mod.rs that encapsulates the pack → send_command → response_payload_as_any pattern repeated across 57 RPC methods. Demonstrate usage in common.rs with 4 converted _raw methods. * feat: complete P1/P2 action items from library assessment - Add beginner examples: hello_kicad.rs and board_inspector.rs - Add README prerequisites section with KiCad IPC API setup guide - Add README examples section with run commands for all 3 examples - Add protocol-contract tests: CMD/RES prefix validation, PCB types catalog - Add module-level rustdoc to all client submodules - All tests pass (default + blocking features) * docs: update assessment report with completed action items Mark resolved: AP-1/AP-2/AP-3 (clippy), DR-2 (anchor), DR-3 (artifact), ST-1 (client.rs split), DR-5 (examples), DR-6 (prerequisites). Mark mitigated: AP-4 (RPC boilerplate via rpc! macro). Update baseline metrics to reflect 11-module client layout. Update risk register, action plan status, and revision history. * chore: finalize tier-1 API docs and modular client cleanup Document the public client and blocking surfaces so strict rustdoc linting passes, while keeping tier-2/3 internals lightly scoped. Also clean stale modularization references and remove leftover split-refactor dead imports/helpers to reduce maintenance drift.
This commit is contained in:
parent
071f22897a
commit
d9644312ef
29
README.md
29
README.md
|
|
@ -15,6 +15,19 @@ Beta. All KiCad v10.0.0 API commands are implemented and tested.
|
||||||
|
|
||||||
- Async API (default): production-ready with full feature parity
|
- Async API (default): production-ready with full feature parity
|
||||||
- Sync/blocking wrapper API (`feature = "blocking"`): production-ready, uses dedicated Tokio runtime thread
|
- Sync/blocking wrapper API (`feature = "blocking"`): production-ready, uses dedicated Tokio runtime thread
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- **Rust 1.70+** (edition 2021)
|
||||||
|
- **KiCad 10.0.0+** running with the IPC API enabled
|
||||||
|
- The `nng` transport library is bundled automatically via [nng-rs](https://crates.io/crates/nng)
|
||||||
|
|
||||||
|
### Enabling the KiCad IPC API
|
||||||
|
|
||||||
|
1. Open KiCad → **Preferences** → **Plugins**
|
||||||
|
2. Check **Enable IPC API**
|
||||||
|
3. Restart KiCad
|
||||||
|
|
||||||
|
The API socket path is auto-detected. Override with `KICAD_API_SOCKET` if needed.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
|
@ -111,6 +124,22 @@ async fn add_track(client: &KiCadClient) -> Result<(), kicad_ipc_rs::KiCadError>
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Run the included examples against a running KiCad instance:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Minimal connection + version check
|
||||||
|
cargo run --example hello_kicad --features blocking
|
||||||
|
|
||||||
|
# Inspect board nets, layers, and origin
|
||||||
|
cargo run --example board_inspector --features blocking
|
||||||
|
|
||||||
|
# Deep-dive into current PCB selection
|
||||||
|
cargo run --example selection_deep_dump --features blocking
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [examples/](examples/) directory for full source.
|
||||||
## KiCad Version Compatibility
|
## KiCad Version Compatibility
|
||||||
|
|
||||||
This crate tracks KiCad releases. When KiCad updates their API, we update within a week. Currently supports KiCad 10.0.0.
|
This crate tracks KiCad releases. When KiCad updates their API, we update within a week. Currently supports KiCad 10.0.0.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,591 @@
|
||||||
|
# Library Assessment Report
|
||||||
|
|
||||||
|
Date: 2026-03-29
|
||||||
|
Repository: `kicad-ipc-rs`
|
||||||
|
Branch assessed: `chore/kicad-v10-stable-protos`
|
||||||
|
Related PR: #23 (`feat: bump vendored KiCad protos to v10.0.0` + follow-up tests)
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This report evaluates the library's structure, correctness, and practical usefulness using a layered review sequence inspired by:
|
||||||
|
|
||||||
|
1. `m15-anti-pattern` (idiomatic/code-smell review)
|
||||||
|
2. `plan-eng-review` (architecture/maintainability review)
|
||||||
|
3. `review` (change-level correctness review)
|
||||||
|
4. `document-release` (docs + release/readiness review)
|
||||||
|
|
||||||
|
No fixes were applied while producing this report.
|
||||||
|
|
||||||
|
## Scope and Method
|
||||||
|
|
||||||
|
### In-scope
|
||||||
|
|
||||||
|
- Core crate code under `src/`
|
||||||
|
- Public docs and usage docs:
|
||||||
|
- `README.md`
|
||||||
|
- `docs/book/src/*.md`
|
||||||
|
- Current PR diff vs `origin/main`
|
||||||
|
- Test/lint/validation signals from local command runs
|
||||||
|
|
||||||
|
### Out-of-scope
|
||||||
|
|
||||||
|
- KiCad upstream implementation internals beyond proto surface validation
|
||||||
|
- Runtime behavior on all KiCad OS/channel combinations
|
||||||
|
- Performance benchmarking
|
||||||
|
|
||||||
|
### Evidence-gathering commands run
|
||||||
|
|
||||||
|
- Test inventory and counts (`rg` on `#[test]` and `#[tokio::test]`)
|
||||||
|
- LOC and public API counts (`wc -l`, `rg -n "pub async fn"`, `rg -n "pub "`)
|
||||||
|
- Diff/PR scope checks (`git diff origin/main...HEAD`)
|
||||||
|
- Validation checks:
|
||||||
|
- `cargo test -q`
|
||||||
|
- `cargo test --features blocking -q`
|
||||||
|
- `cargo clippy --all-targets --all-features -- -D warnings`
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
Overall status: **Good functional correctness with strong protocol-awareness, moderate maintainability risk, and moderate documentation/discoverability risk.**
|
||||||
|
|
||||||
|
- Correctness: **Strong** for current scope and PR #23 changes.
|
||||||
|
- Architecture: **Solid layering** with a **recently modularized client architecture (formerly monolithic `client.rs`)**.
|
||||||
|
- API usefulness: Good typed surface, with intentional raw escape hatches.
|
||||||
|
- Release/docs hygiene: Version-snippet drift, cross-link integrity, onboarding examples, and prerequisites have all been addressed. Remaining weakness is deeper rustdoc method-level coverage.
|
||||||
|
- Test posture: Healthy and improving; protocol-contract tests were a high-value addition.
|
||||||
|
|
||||||
|
## Baseline Metrics
|
||||||
|
|
||||||
|
### Code footprint
|
||||||
|
|
||||||
|
#### Full non-generated source tree (LOC)
|
||||||
|
|
||||||
|
- `src/client/mod.rs`: 230
|
||||||
|
- `src/client/common.rs`: 354
|
||||||
|
- `src/client/board.rs`: 360
|
||||||
|
- `src/client/items.rs`: 426
|
||||||
|
- `src/client/selection.rs`: 181
|
||||||
|
- `src/client/document.rs`: 191
|
||||||
|
- `src/client/geometry.rs`: 241
|
||||||
|
- `src/client/mappers.rs`: 1148
|
||||||
|
- `src/client/decode.rs`: 549
|
||||||
|
- `src/client/format.rs`: 349
|
||||||
|
- `src/client/tests.rs`: 1230
|
||||||
|
- `src/model/board.rs`: 746
|
||||||
|
- `src/blocking.rs`: 667
|
||||||
|
- `src/model/common.rs`: 550
|
||||||
|
- `src/transport.rs`: 126
|
||||||
|
- `src/lib.rs`: 112
|
||||||
|
- `src/envelope.rs`: 110
|
||||||
|
- `src/error.rs`: 90
|
||||||
|
- `src/proto/mod.rs`: 38
|
||||||
|
- `src/commands/mod.rs`: 4
|
||||||
|
- `src/model/mod.rs`: 2
|
||||||
|
- `src/kicad_api_version.rs`: 2
|
||||||
|
- `src/commands/project.rs`: 2
|
||||||
|
- `src/commands/editor.rs`: 2
|
||||||
|
- `src/commands/board.rs`: 2
|
||||||
|
- `src/commands/base.rs`: 2
|
||||||
|
|
||||||
|
**Non-generated total:** 7714 LOC
|
||||||
|
**Generated proto total:** 4863 LOC
|
||||||
|
**Overall total:** 12577 LOC
|
||||||
|
|
||||||
|
Interpretation: Formerly concentrated in a single 5448-LOC file, the client module has been split into 11 focused domain modules.
|
||||||
|
|
||||||
|
### Dependency and module surface snapshot
|
||||||
|
|
||||||
|
- Direct dependencies: 6 (`nng`, `prost`, `prost-types`, `thiserror`, `tokio`, `tracing`)
|
||||||
|
- Public modules exported from `lib.rs`: 7
|
||||||
|
- `client`
|
||||||
|
- `commands`
|
||||||
|
- `envelope`
|
||||||
|
- `error`
|
||||||
|
- `model`
|
||||||
|
- `transport`
|
||||||
|
- `blocking` (feature-gated)
|
||||||
|
- Total public API items (excluding generated code): 139
|
||||||
|
|
||||||
|
Interpretation: API breadth is substantial relative to crate size; discoverability and consistency controls are important.
|
||||||
|
|
||||||
|
### API surface size
|
||||||
|
|
||||||
|
- Public async methods in `KiCadClient`: 107
|
||||||
|
- Public blocking methods in `KiCadClientBlocking`: 26 explicitly counted wrappers (plus macro-generated parity set)
|
||||||
|
|
||||||
|
Interpretation: This is a broad API surface for a single crate module; drift control and discoverability are key concerns.
|
||||||
|
|
||||||
|
### Test inventory (unit tests in `src/`)
|
||||||
|
|
||||||
|
- `src/client/tests.rs`: 65
|
||||||
|
- `src/blocking.rs`: 7
|
||||||
|
- `src/model/common.rs`: 6
|
||||||
|
- `src/model/board.rs`: 4
|
||||||
|
- `src/envelope.rs`: 2
|
||||||
|
|
||||||
|
Total identified unit tests: 84
|
||||||
|
|
||||||
|
Interpretation: Coverage appears substantial for a library of this size, with strongest emphasis on client behavior and mapping.
|
||||||
|
|
||||||
|
## Pass 1: `m15-anti-pattern` Findings
|
||||||
|
|
||||||
|
### What looks good
|
||||||
|
|
||||||
|
- No `unsafe` usage in `src/`, reducing a major class of memory-safety risk.
|
||||||
|
- Error mapping is explicit and specific in `src/error.rs`.
|
||||||
|
|
||||||
|
### Verified clean signals
|
||||||
|
|
||||||
|
- **Zero `.unwrap()` calls in production code** (confirmed via exhaustive grep).
|
||||||
|
- **All 56 `.expect()` calls are in test code only** (zero in production paths).
|
||||||
|
- **Zero production `panic!` usage** (panic usage is confined to test modules under `src/client/tests.rs`).
|
||||||
|
- **No TODO/FIXME/HACK/XXX markers** in non-generated source.
|
||||||
|
|
||||||
|
### Notable anti-pattern/style findings
|
||||||
|
|
||||||
|
#### AP-1: `clone_on_copy` in production mapping path
|
||||||
|
|
||||||
|
- Evidence: previously observed in client mapping code before modularization.
|
||||||
|
- Impact: Low runtime impact, but signals unnecessary ownership noise and can obscure intent.
|
||||||
|
- Severity: Low
|
||||||
|
- **Status: RESOLVED** (commit 5d3bb4b)
|
||||||
|
|
||||||
|
#### AP-2: Strict clippy fails on generated protobuf enums
|
||||||
|
|
||||||
|
- Evidence:
|
||||||
|
- `src/proto/generated/kiapi.common.rs:56`
|
||||||
|
- `src/proto/generated/kiapi.common.commands.rs:424`, `:459`, `:490` (and similar)
|
||||||
|
- Lint class: `clippy::enum_variant_names` under `-D warnings`
|
||||||
|
- Impact: High CI/tooling friction if strict clippy is expected to pass globally.
|
||||||
|
- Severity: Medium (process/tooling risk)
|
||||||
|
- **Status: RESOLVED** — added targeted `#[allow(clippy::enum_variant_names)]` in `src/proto/mod.rs` (commit 5d3bb4b)
|
||||||
|
|
||||||
|
#### AP-3: Test-style bool asserts flagged
|
||||||
|
|
||||||
|
- Evidence: client unit tests under `src/client/tests.rs`.
|
||||||
|
- Impact: Low; style-level issue only.
|
||||||
|
- Severity: Low
|
||||||
|
- **Status: RESOLVED** (commit 5d3bb4b)
|
||||||
|
|
||||||
|
#### AP-4: Heavy repeated RPC boilerplate in client module
|
||||||
|
|
||||||
|
- Repeated pattern appears across many methods:
|
||||||
|
1. Build command
|
||||||
|
2. `send_command(envelope::pack_any(&command, CMD_*))`
|
||||||
|
3. `response_payload_as_any(response, RES_*)`
|
||||||
|
4. `decode_any(...)`
|
||||||
|
- Evidence:
|
||||||
|
- Seen across `src/client/common.rs`, `src/client/board.rs`, `src/client/items.rs`, `src/client/document.rs`.
|
||||||
|
- Impact: Boilerplate proliferation increases maintenance drag and inconsistency risk.
|
||||||
|
- Severity: Medium
|
||||||
|
- **Status: MITIGATED** — `rpc!` dispatch macro added and demonstrated in 4 methods (commit bda2ed6). Full conversion available for future work.
|
||||||
|
|
||||||
|
#### AP-5: Silenced results via `let _ =` in production code
|
||||||
|
|
||||||
|
- Evidence:
|
||||||
|
- Client module methods in `src/client/common.rs`, `src/client/board.rs`, and `src/client/document.rs`
|
||||||
|
- `src/transport.rs:36` (channel send during shutdown)
|
||||||
|
- `src/blocking.rs`: `:41`, `:46`, `:77`, `:103`
|
||||||
|
- Notes: Some cases are intentional (e.g., benign send failure during shutdown), but several cases could hide useful operational failures.
|
||||||
|
- Impact: Potential silent failure paths and debugging opacity.
|
||||||
|
- Severity: Low-Medium
|
||||||
|
|
||||||
|
#### AP-6: Pervasive unchecked `as i32` casts for protobuf enum discriminants
|
||||||
|
|
||||||
|
- Evidence: numerous production instances across `src/client/mod.rs`, `src/client/mappers.rs`, and `src/model/common.rs`.
|
||||||
|
- Notes: This is common in prost-backed code, but it is still unchecked narrowing.
|
||||||
|
- Impact: Low in current protocol-constrained context; type-safety remains weaker than explicit conversion helpers.
|
||||||
|
- Severity: Low
|
||||||
|
|
||||||
|
### Anti-pattern conclusion
|
||||||
|
|
||||||
|
No major architectural anti-patterns like pervasive `unwrap` in production paths, unsafe shortcuts, or panic-driven control flow were found. Primary concern is maintainability/process friction: boilerplate repetition, strict clippy policy mismatch with generated code, and a handful of silent-result patterns.
|
||||||
|
|
||||||
|
## Pass 2: `plan-eng-review` Findings (Structure and Maintainability)
|
||||||
|
|
||||||
|
### Structural strengths
|
||||||
|
|
||||||
|
- Clean conceptual layering reflected in module organization:
|
||||||
|
- `transport` (IPC boundary)
|
||||||
|
- `envelope` (protobuf Any/type URL handling)
|
||||||
|
- client-level typed wrappers and conversions
|
||||||
|
- Blocking facade includes a strong parity guard:
|
||||||
|
- `src/blocking.rs:586` (`sync_wrapper_covers_async_method_names`)
|
||||||
|
- This is an excellent protection against async/blocking drift.
|
||||||
|
- Rich typed models provide ergonomic APIs over raw protobuf payloads:
|
||||||
|
- `src/model/board.rs`
|
||||||
|
- `src/model/common.rs`
|
||||||
|
|
||||||
|
### Transport architecture
|
||||||
|
|
||||||
|
- Implemented in a single file: `src/transport.rs` (126 LOC), not a transport directory.
|
||||||
|
- Uses an `nng` `Req0` socket with an async-to-blocking bridge.
|
||||||
|
- Tokio MPSC queue (capacity = 64) feeds a dedicated OS worker thread.
|
||||||
|
- Worker performs blocking `socket_roundtrip` (send then recv).
|
||||||
|
- Async side awaits completion via oneshot response channels.
|
||||||
|
- Timeout and transport failures are clearly mapped to `KiCadError::{Timeout, TransportSend, TransportReceive, Connection}`.
|
||||||
|
|
||||||
|
### Feature flag architecture
|
||||||
|
|
||||||
|
- `default = ["async"]`
|
||||||
|
- `async = ["dep:nng", "dep:prost", "dep:prost-types", "dep:tokio"]`
|
||||||
|
- `blocking = ["async"]` (additive, not replacement)
|
||||||
|
- `tracing = ["dep:tracing"]`
|
||||||
|
- Blocking facade runs a dedicated single-thread Tokio runtime worker and dispatches sync calls through a bounded channel.
|
||||||
|
|
||||||
|
### Model layer cross-dependencies
|
||||||
|
|
||||||
|
- `model::common` depends on board-layer types (`PcbItem`, `Vector2Nm`, etc.).
|
||||||
|
- Effective layering is common → board-aware, not fully independent.
|
||||||
|
- This is pragmatically acceptable today, but worth noting if future domain split/modularization is pursued.
|
||||||
|
|
||||||
|
### Structural risks
|
||||||
|
|
||||||
|
#### ST-1: Monolithic client module
|
||||||
|
|
||||||
|
- Evidence: formerly monolithic `src/client.rs` at 5448 LOC with 107 public async methods.
|
||||||
|
- Why it matters:
|
||||||
|
- Larger review blast radius for changes.
|
||||||
|
- Mixed responsibilities (command dispatch + mapping + helpers + tests) in one file.
|
||||||
|
- Harder onboarding and higher chance of incidental coupling.
|
||||||
|
- Severity: Medium
|
||||||
|
|
||||||
|
##### Natural splitting points for `client.rs`
|
||||||
|
|
||||||
|
- `client/common.rs`: ping/version/paths/open docs/run_action/text/netclass (~lines 359-666)
|
||||||
|
- `client/items.rs`: item CRUD, item decoding, by-id/by-type queries (~lines 671-861, 1211-1409)
|
||||||
|
- `client/board.rs`: board layers/origin/stackup/appearance/nets (~lines 885-1040, 1572-1744)
|
||||||
|
- `client/selection.rs`: selection mutation + summaries/details (~lines 1047-1201)
|
||||||
|
- `client/geometry.rs`: text extents/shapes, bounding box, hit test, pad polygon (~lines 1426-1572, 1879-1954)
|
||||||
|
- `client/document.rs`: save/revert/string serialization/title block (~lines 1750-1865)
|
||||||
|
- `client/mappers.rs`: pure proto↔model mapping helpers (post-1954)
|
||||||
|
|
||||||
|
**Status: RESOLVED** — client.rs has been split into 11 domain modules under `src/client/` (commit 028aff9). All public API signatures preserved.
|
||||||
|
|
||||||
|
#### ST-2: Public `commands::*` modules appear as placeholders
|
||||||
|
|
||||||
|
- Evidence:
|
||||||
|
- `src/commands/base.rs`
|
||||||
|
- `src/commands/board.rs`
|
||||||
|
- `src/commands/editor.rs`
|
||||||
|
- `src/commands/project.rs`
|
||||||
|
- Each contains only a trivial empty struct.
|
||||||
|
- Why it matters: Public API surface may imply supported low-level builder functionality that is not yet substantive.
|
||||||
|
- Severity: Low to Medium (usability/signaling risk)
|
||||||
|
|
||||||
|
#### ST-3: Very broad public API requires stronger discoverability discipline
|
||||||
|
|
||||||
|
- With 100+ async methods, docs quality and examples become critical for practical use.
|
||||||
|
- Missing docs warnings and uneven method-level docs indicate discoverability debt.
|
||||||
|
- Severity: Medium
|
||||||
|
|
||||||
|
### Structure conclusion
|
||||||
|
|
||||||
|
Architecture is fundamentally sound, but maintainability risk is rising due to centralization and API breadth. Modularizing client domains and reducing repeated RPC boilerplate are the highest-value structural improvements.
|
||||||
|
|
||||||
|
## Pass 3: `review` Findings (PR #23 Correctness)
|
||||||
|
|
||||||
|
### What changed in PR scope
|
||||||
|
|
||||||
|
Diff vs `origin/main` (10 files changed):
|
||||||
|
|
||||||
|
- `kicad` submodule pin update to KiCad `10.0.0`
|
||||||
|
- Generated proto refresh:
|
||||||
|
- `src/proto/generated/kiapi.board.commands.rs`
|
||||||
|
- New method wiring:
|
||||||
|
- `src/client/board.rs` (`GetBoardLayerName` request/response constants + method)
|
||||||
|
- `src/blocking.rs` parity method
|
||||||
|
- Coverage/document updates:
|
||||||
|
- `README.md`
|
||||||
|
- `docs/book/src/intro.md`
|
||||||
|
- `docs/book/src/validation.md`
|
||||||
|
- `src/lib.rs`
|
||||||
|
- `src/kicad_api_version.rs`
|
||||||
|
- `test-scripts/kicad-ipc-cli.rs`
|
||||||
|
|
||||||
|
### Correctness assessment
|
||||||
|
|
||||||
|
#### CR-1: KiCad v10 stable proto delta appears correctly applied
|
||||||
|
|
||||||
|
- The newly-added command (`GetBoardLayerName`) is reflected in generated types and wrapped in high-level API.
|
||||||
|
|
||||||
|
#### CR-2: Protocol-contract tests are meaningful and non-trivial
|
||||||
|
|
||||||
|
Added tests verify runtime `type_url` contracts, which the Rust type system cannot enforce by itself:
|
||||||
|
|
||||||
|
- decode succeeds on expected type URL
|
||||||
|
- decode fails on mismatched type URL
|
||||||
|
- command Any packing uses expected proto command name
|
||||||
|
|
||||||
|
This is a strong testing choice for IPC/protobuf Any boundaries.
|
||||||
|
|
||||||
|
#### CR-3: Blocking parity maintained
|
||||||
|
|
||||||
|
- New async method has matching blocking exposure and remains protected by parity test strategy.
|
||||||
|
|
||||||
|
### Correctness conclusion
|
||||||
|
|
||||||
|
PR #23 quality is good. Changes are focused, functionally coherent, and backed by targeted tests that protect real protocol failure modes.
|
||||||
|
|
||||||
|
## Pass 4: `document-release` Findings (Usefulness and Release Readiness)
|
||||||
|
|
||||||
|
### Positive documentation signals
|
||||||
|
|
||||||
|
- README contains detailed compatibility matrix and runtime notes.
|
||||||
|
- Book includes API reference links and practical usage patterns.
|
||||||
|
- Validation commands are clearly documented.
|
||||||
|
- Version snippet drift previously identified in DR-1 has been corrected (see **Resolved Issues**).
|
||||||
|
|
||||||
|
### Active documentation and release-readiness findings
|
||||||
|
|
||||||
|
#### DR-2: Broken anchor in `validation.md`
|
||||||
|
|
||||||
|
- `docs/book/src/validation.md:21` links to README anchor `#kicad-v1000-api-completion-matrix`
|
||||||
|
- README heading is `## KiCad v10.0.0 API Reference` (`README.md:118`)
|
||||||
|
- Anchor slug does not match current heading
|
||||||
|
- Severity: Low (broken cross-reference)
|
||||||
|
- **Status: RESOLVED** (commit 5d3bb4b)
|
||||||
|
|
||||||
|
#### DR-3: Filesystem artifact in docs tree
|
||||||
|
|
||||||
|
- `docs/book/src/https:/docs.rs/` was investigated as a potential literal directory artifact.
|
||||||
|
- Investigation confirmed the artifact does not exist on this branch.
|
||||||
|
- Severity: Low (docs hygiene)
|
||||||
|
- **Status: RESOLVED** — investigation confirmed the artifact does not exist on this branch.
|
||||||
|
|
||||||
|
#### DR-4: Rustdoc coverage gap in client Tier 1 surface
|
||||||
|
|
||||||
|
- 120 public items across `src/client/*.rs`
|
||||||
|
- 47 documented (39% coverage)
|
||||||
|
- 73 public items undocumented
|
||||||
|
- Gaps notably include several `*_raw` variants and some typed wrappers
|
||||||
|
- Evidence examples of undocumented public methods:
|
||||||
|
- `src/client/common.rs`
|
||||||
|
- `src/client/board.rs`
|
||||||
|
- `src/client/items.rs`
|
||||||
|
- `src/client/selection.rs`
|
||||||
|
- Severity: Medium (API discoverability)
|
||||||
|
|
||||||
|
#### DR-5: Narrow examples set
|
||||||
|
|
||||||
|
- Only one example: `examples/selection_deep_dump.rs` (632 LOC)
|
||||||
|
- Current example is advanced and blocking-feature-gated
|
||||||
|
- No beginner-oriented examples (e.g., connect+ping, simple list/query)
|
||||||
|
- Severity: Medium (onboarding friction)
|
||||||
|
- **Status: RESOLVED** — added `hello_kicad.rs` (connect+ping+version) and `board_inspector.rs` (nets/layers/origin) examples (commit 6efc241)
|
||||||
|
|
||||||
|
#### DR-6: README missing explicit prerequisites section
|
||||||
|
|
||||||
|
- No dedicated prerequisites section explicitly stating KiCad runtime requirements (running KiCad with IPC available/enabled)
|
||||||
|
- Runtime preconditions are discoverable via book quickstart, but not surfaced early in README
|
||||||
|
- Severity: Low-Medium (onboarding clarity)
|
||||||
|
- **Status: RESOLVED** — added Prerequisites section with KiCad IPC API setup guide and `KICAD_API_SOCKET` override note (commit 6efc241)
|
||||||
|
|
||||||
|
### Documentation conclusion
|
||||||
|
|
||||||
|
Documentation quality is generally strong, and version alignment has materially improved. Remaining gaps are low-to-medium severity but user-visible: cross-link correctness, explicit prerequisites, broader examples, and improved rustdoc coverage for the Tier 1 API surface.
|
||||||
|
|
||||||
|
## Quality Gates and Validation Status
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- `cargo test -q`: pass
|
||||||
|
- `cargo test --features blocking -q`: pass
|
||||||
|
|
||||||
|
### Linting
|
||||||
|
|
||||||
|
- `cargo clippy --all-targets --all-features -- -D warnings`: fails
|
||||||
|
|
||||||
|
Observed root causes:
|
||||||
|
|
||||||
|
1. Generated proto enum naming lints under strict clippy
|
||||||
|
2. A few fixable local style issues (clone-on-copy, bool assert style in tests)
|
||||||
|
|
||||||
|
Implication: Either clippy policy needs explicit generated-code handling, or strict global clippy will remain noisy/fragile.
|
||||||
|
|
||||||
|
## Resolved Issues
|
||||||
|
|
||||||
|
### DR-1: Stale version snippets in docs (resolved)
|
||||||
|
|
||||||
|
Previously reported version drift has been fixed and is no longer an active risk.
|
||||||
|
|
||||||
|
Resolved evidence:
|
||||||
|
|
||||||
|
- `README.md:27` → `0.4.1` ✓
|
||||||
|
- `README.md:67` → `0.4.1` ✓
|
||||||
|
- `docs/book/src/quickstart.md:15` → `0.4.1` ✓
|
||||||
|
- `docs/book/src/quickstart.md:42` → `0.4.1` ✓
|
||||||
|
- `Cargo.toml` crate version → `0.4.1` ✓
|
||||||
|
- `CHANGELOG.md` includes `[0.4.1]` entry ✓
|
||||||
|
|
||||||
|
Impact: The highest-friction onboarding mismatch from the initial report has been addressed.
|
||||||
|
|
||||||
|
### DR-2: Broken anchor in validation.md (resolved)
|
||||||
|
|
||||||
|
Fixed in commit 5d3bb4b. Anchor updated to match current README heading.
|
||||||
|
|
||||||
|
### DR-3: Filesystem artifact (resolved)
|
||||||
|
|
||||||
|
Investigation confirmed the `docs/book/src/https:` directory does not exist on this branch.
|
||||||
|
|
||||||
|
### AP-1/AP-2/AP-3: Clippy lint findings (resolved)
|
||||||
|
|
||||||
|
All three clippy findings fixed in commit 5d3bb4b.
|
||||||
|
|
||||||
|
### ST-1: Monolithic client.rs (resolved)
|
||||||
|
|
||||||
|
Split into 11 domain modules in commit 028aff9. No public API changes.
|
||||||
|
|
||||||
|
### DR-5/DR-6: Examples and prerequisites (resolved)
|
||||||
|
|
||||||
|
Two beginner examples and a README prerequisites section added in commit 6efc241.
|
||||||
|
|
||||||
|
## Risk Register
|
||||||
|
|
||||||
|
| ID | Risk | Area | Severity | Likelihood | Notes |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| R2 | Monolithic `client.rs` slows safe evolution | Structure | Medium | High | RESOLVED — Split into 11 domain modules (commit 028aff9) |
|
||||||
|
| R3 | Strict clippy friction due to generated code | Process | Medium | High | Reproducible with current strict command |
|
||||||
|
| R4 | Public placeholder command modules confuse users | API clarity | Low/Med | Medium | Can be fixed with docs or visibility adjustment |
|
||||||
|
| R5 | Missing docs on public Tier 1 items in `src/client/*` | DX/discoverability | Medium | Medium | 73/120 public items undocumented (39% documented) |
|
||||||
|
| R6 | Broken doc links/anchors | Docs | Low | Medium | RESOLVED — Anchor fixed (commit 5d3bb4b) |
|
||||||
|
| R7 | Filesystem artifact in docs tree | Hygiene | Low | Low | RESOLVED — Artifact confirmed nonexistent |
|
||||||
|
| R8 | Narrow examples coverage | Onboarding | Medium | High | RESOLVED — Two beginner examples added (commit 6efc241) |
|
||||||
|
| R9 | Repeated RPC boilerplate | Maintainability | Medium | High | MITIGATED — `rpc!` macro added (commit bda2ed6) |
|
||||||
|
|
||||||
|
## Prioritized Action Plan (Report-only)
|
||||||
|
|
||||||
|
### P0: Immediate (highest ROI)
|
||||||
|
|
||||||
|
1. ✅ Done — Fix broken `validation.md` anchor to correctly reference current README heading.
|
||||||
|
2. ⬜ Open — Document newly added and existing Tier 1 public methods; target **80%+ rustdoc coverage** for Tier 1 public methods.
|
||||||
|
3. ✅ Done — Define clippy policy for generated files (allowlist/scope strategy) and document it.
|
||||||
|
4. ✅ Done — Clean obvious non-generated clippy findings (`clone_on_copy`, bool assert style in tests).
|
||||||
|
5. ✅ Done — Remove `docs/book/src/https:` filesystem artifact from docs tree (investigation confirmed nonexistent on this branch).
|
||||||
|
|
||||||
|
Expected outcome: Cleaner user navigation, better first-run success, improved CI signal quality, reduced contributor confusion.
|
||||||
|
|
||||||
|
### P1: Maintainability upgrades
|
||||||
|
|
||||||
|
1. ✅ Done — Split the former monolithic client file by functional domains while preserving public API signatures, using module seams under `src/client/`:
|
||||||
|
- `client/common.rs`
|
||||||
|
- `client/items.rs`
|
||||||
|
- `client/board.rs`
|
||||||
|
- `client/selection.rs`
|
||||||
|
- `client/geometry.rs`
|
||||||
|
- `client/document.rs`
|
||||||
|
- `client/mappers.rs`
|
||||||
|
- `client/decode.rs`
|
||||||
|
- `client/format.rs`
|
||||||
|
- `client/tests.rs`
|
||||||
|
2. ⬜ Open — Group command/response type URL constants near their domain methods.
|
||||||
|
3. ✅ Done — Keep and extend protocol-contract test helpers to reduce repeated literal contract strings.
|
||||||
|
4. ✅ Done — Extract a generic typed RPC dispatch helper to reduce repeated `send_command`/`pack_any`/`response_payload_as_any`/`decode_any` boilerplate.
|
||||||
|
5. ✅ Done — Add 2–3 beginner-friendly examples (e.g., connect+ping, list-nets, simple query).
|
||||||
|
|
||||||
|
Expected outcome: Smaller review units, lower regression risk, reduced boilerplate drift, faster onboarding.
|
||||||
|
|
||||||
|
### P2: API clarity and polish
|
||||||
|
|
||||||
|
1. Clarify intent of public `commands::*` modules (document as placeholders or reduce visibility until substantive).
|
||||||
|
2. Add a concise versioning model section (crate version vs proto pin vs tested KiCad runtime).
|
||||||
|
3. Add one focused quickstart snippet showing `get_board_layer_name` usage.
|
||||||
|
4. ✅ Done — Add an explicit README **Prerequisites** section describing KiCad runtime requirements.
|
||||||
|
5. ⬜ Partial — Improve rustdoc coverage to 80%+ for Tier 1 API surface (module-level rustdoc added across client submodules; deeper method-level coverage still needed).
|
||||||
|
6. Audit and fix all cross-document links between mdBook and README.
|
||||||
|
|
||||||
|
Expected outcome: Reduced user misinterpretation and smoother docs-driven adoption.
|
||||||
|
|
||||||
|
## Recommended Documentation Policy (Three Tiers)
|
||||||
|
|
||||||
|
Given the current API breadth, a tiered documentation policy is the best balance between usability and maintenance.
|
||||||
|
|
||||||
|
### Tier definitions
|
||||||
|
|
||||||
|
1. Tier 1 (primary client-facing API)
|
||||||
|
- Examples: `KiCadClient`, `KiCadClientBlocking`, core typed models used by normal consumers.
|
||||||
|
- Policy: Fully documented (method docs + parameter behavior + return semantics + error notes where relevant).
|
||||||
|
- Goal: New users should succeed from rustdoc + README/book without reading internals.
|
||||||
|
|
||||||
|
2. Tier 2 (advanced public API)
|
||||||
|
- Examples: advanced helper modules/surfaces intended for power users.
|
||||||
|
- Policy: Public but intentionally light docs.
|
||||||
|
- Minimum docs: one module-level explanation describing intended audience and "prefer Tier 1 first" guidance.
|
||||||
|
|
||||||
|
3. Tier 3 (low-level/raw plumbing)
|
||||||
|
- Examples: transport/protobuf-oriented internals exposed for specialized integration.
|
||||||
|
- Policy: Public for escape hatches, minimal docs, and clearly labeled as advanced.
|
||||||
|
- Optional: hide from rustdoc navigation via `#[doc(hidden)]` for especially noisy surfaces while keeping symbols public.
|
||||||
|
|
||||||
|
### Lint and docs strategy
|
||||||
|
|
||||||
|
1. Keep strict documentation quality for Tier 1.
|
||||||
|
- Continue `#![warn(missing_docs)]` and drive Tier 1 toward zero missing-doc warnings.
|
||||||
|
|
||||||
|
2. Scope missing-doc relaxations only to Tier 2/3 module boundaries.
|
||||||
|
- Prefer local `#[allow(missing_docs)]` on specific modules over crate-wide suppression.
|
||||||
|
- This preserves signal for user-facing APIs while reducing low-value warning noise.
|
||||||
|
|
||||||
|
3. Add clear module-level labels for advanced surfaces.
|
||||||
|
- Recommended wording pattern:
|
||||||
|
- "Advanced API surface"
|
||||||
|
- "May change more frequently than Tier 1"
|
||||||
|
- "Prefer Tier 1 APIs unless you need lower-level control"
|
||||||
|
|
||||||
|
### Why this approach is recommended
|
||||||
|
|
||||||
|
- It aligns with user needs: newcomers get high-quality guidance where it matters.
|
||||||
|
- It avoids documentation bloat in internal/advanced layers.
|
||||||
|
- It keeps compiler/rustdoc warnings actionable instead of overwhelming.
|
||||||
|
- It preserves public extensibility without forcing exhaustive docs for every low-level symbol.
|
||||||
|
|
||||||
|
## Suggested Acceptance Criteria for Follow-up Work
|
||||||
|
|
||||||
|
### For documentation consistency and integrity
|
||||||
|
|
||||||
|
- README/book cross-links resolve correctly.
|
||||||
|
- No malformed artifact directories remain under docs source.
|
||||||
|
- README includes an explicit prerequisites section for KiCad IPC runtime conditions.
|
||||||
|
- Tier 1 public API rustdoc coverage reaches 80%+.
|
||||||
|
|
||||||
|
### For lint/process hygiene
|
||||||
|
|
||||||
|
- `cargo clippy --all-targets --all-features -- -D warnings` is either:
|
||||||
|
- fully green, or
|
||||||
|
- intentionally scoped with documented generated-code exemptions.
|
||||||
|
|
||||||
|
### For architecture improvements
|
||||||
|
|
||||||
|
- Client module split into coherent domain submodules under `src/client/`.
|
||||||
|
- Repeated RPC boilerplate consolidated into typed helper(s) where practical.
|
||||||
|
- No public API breakage in function names/signatures.
|
||||||
|
- Existing parity and protocol tests remain green.
|
||||||
|
|
||||||
|
## Detailed Notes and Evidence Pointers
|
||||||
|
|
||||||
|
- Core API and layering: `src/lib.rs`
|
||||||
|
- Main API implementation and constants: `src/client/mod.rs`
|
||||||
|
- Blocking parity guard test: `src/blocking.rs:586`
|
||||||
|
- Transport bridge implementation: `src/transport.rs`
|
||||||
|
- Error boundary taxonomy: `src/error.rs`
|
||||||
|
- Generated proto lint hotspot examples:
|
||||||
|
- `src/proto/generated/kiapi.common.rs:56`
|
||||||
|
- `src/proto/generated/kiapi.common.commands.rs:424`
|
||||||
|
- Documentation issues:
|
||||||
|
- `docs/book/src/validation.md:21`
|
||||||
|
- `README.md:118`
|
||||||
|
- `docs/book/src/validation.md:14`
|
||||||
|
- Rustdoc coverage evidence samples (undocumented public methods):
|
||||||
|
- `src/client/common.rs`
|
||||||
|
- `src/client/board.rs`
|
||||||
|
- `src/client/items.rs`
|
||||||
|
- `src/client/selection.rs`
|
||||||
|
|
||||||
|
## Final Assessment
|
||||||
|
|
||||||
|
The library is in a strong position functionally and continues to show disciplined protocol-aware testing. The most impactful near-term improvements are rustdoc depth for Tier 1 APIs and lint/process cleanup after the client modularization. Medium-term effort should focus on continuing RPC boilerplate consolidation and keeping module boundaries clean as API breadth grows.
|
||||||
|
|
||||||
|
## Report Revision History
|
||||||
|
|
||||||
|
- 2026-03-29: Initial report generated (partial)
|
||||||
|
- 2026-03-29: Comprehensive completion pass — verified all metrics against codebase, expanded anti-pattern scan (3→6 findings + verified clean signals), added transport/feature-flag/model architecture details, identified 5 new documentation issues, corrected resolved DR-1, expanded risk register (5→9 entries), and updated action plan
|
||||||
|
- 2026-03-29: Implementation pass — completed P0 fixes, client.rs modularization, `rpc!` macro, beginner examples, README prerequisites/examples sections, protocol-contract tests, and module-level rustdoc across all client submodules
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
# Selection API Lossiness Audit + Execution Plan
|
||||||
|
|
||||||
|
Goal: close data-loss gaps between KiCad protobuf payloads and public `kicad-ipc-rs` selection APIs.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
- `GetSelection` family:
|
||||||
|
- `get_selection_raw`
|
||||||
|
- `get_selection`
|
||||||
|
- `get_selection_details`
|
||||||
|
- `get_selection_summary`
|
||||||
|
- `add/remove/clear_selection` typed wrappers
|
||||||
|
- `get_selection_as_string`
|
||||||
|
|
||||||
|
## Source Anchors (do not re-discover)
|
||||||
|
|
||||||
|
- Proto commands:
|
||||||
|
- [`kicad/api/proto/common/commands/editor_commands.proto:338`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/kicad/api/proto/common/commands/editor_commands.proto:338) (`GetSelection`)
|
||||||
|
- [`kicad/api/proto/common/commands/editor_commands.proto:349`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/kicad/api/proto/common/commands/editor_commands.proto:349) (`SelectionResponse`)
|
||||||
|
- [`kicad/api/proto/common/commands/editor_commands.proto:355`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/kicad/api/proto/common/commands/editor_commands.proto:355) (`AddToSelection`)
|
||||||
|
- [`kicad/api/proto/common/commands/editor_commands.proto:364`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/kicad/api/proto/common/commands/editor_commands.proto:364) (`RemoveFromSelection`)
|
||||||
|
- [`kicad/api/proto/common/commands/editor_commands.proto:373`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/kicad/api/proto/common/commands/editor_commands.proto:373) (`ClearSelection`)
|
||||||
|
- [`kicad/api/proto/common/commands/editor_commands.proto:424`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/kicad/api/proto/common/commands/editor_commands.proto:424) (`SavedSelectionResponse`)
|
||||||
|
- Client flow:
|
||||||
|
- `src/client/selection.rs` (`get_selection_raw`, `get_selection_details`, `get_selection`, `get_selection_summary`, `add_to_selection`, `clear_selection`, `remove_from_selection`, `get_selection_as_string`, `summarize_selection`, `summarize_item_details`)
|
||||||
|
- `src/client/decode.rs` (`decode_pcb_item`)
|
||||||
|
- Public model bottleneck:
|
||||||
|
- [`src/model/board.rs:389`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/model/board.rs:389) onward (`Pcb*` structs + `PcbItem`)
|
||||||
|
- [`src/model/common.rs:194`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/model/common.rs:194) (`SelectionSummary`)
|
||||||
|
- [`src/model/common.rs:203`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/model/common.rs:203) (`SelectionItemDetail`)
|
||||||
|
- Relevant proto item schemas:
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:19`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:19) (`Track`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:39`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:39) (`Arc`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:227`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:227) (`Via`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:305`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:305) (`Pad`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:420`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:420) (`Zone`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:520`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:520) (`Dimension`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:580`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:580) (`Group`)
|
||||||
|
- [`src/proto/generated/kiapi.board.types.rs:705`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.board.types.rs:705) (`FootprintInstance`)
|
||||||
|
- [`src/proto/generated/kiapi.common.types.rs:541`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.common.types.rs:541) (`Text`)
|
||||||
|
- [`src/proto/generated/kiapi.common.types.rs:554`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.common.types.rs:554) (`TextBox`)
|
||||||
|
- [`src/proto/generated/kiapi.common.types.rs:634`](/Users/milindsharma/Developer/kicad-oss/kicad-ipc-rs/src/proto/generated/kiapi.common.types.rs:634) (`GraphicShape`)
|
||||||
|
|
||||||
|
## Current State: What Is Lossy vs Not
|
||||||
|
|
||||||
|
### Not lossy
|
||||||
|
|
||||||
|
- `get_selection_raw` returns `SelectionResponse.items` directly (`Vec<Any>`). No internal field drop.
|
||||||
|
- `*_selection_raw` variants for add/remove/clear preserve raw payload when server returns `SelectionResponse`.
|
||||||
|
|
||||||
|
### Lossy API layers
|
||||||
|
|
||||||
|
- `get_selection_summary`: compresses all item payloads into counts by `type_url`.
|
||||||
|
- `get_selection_details`: flattens into human/debug string + byte length; no structured fields.
|
||||||
|
- `get_selection`: decodes into reduced `PcbItem` models with many fields omitted.
|
||||||
|
- `add_to_selection` / `remove_from_selection` / `clear_selection`: typed wrappers return summary only.
|
||||||
|
- `get_selection_as_string`: drops `SavedSelectionResponse.ids`; returns `contents` only.
|
||||||
|
- `GetSelection.types` filter exists in proto, but no public method exposes it (always empty in current code).
|
||||||
|
|
||||||
|
## Loss Inventory by Item Type (proto -> public typed)
|
||||||
|
|
||||||
|
- `Track`: drops `locked`.
|
||||||
|
- `Arc`: drops `locked`.
|
||||||
|
- `Via`: drops `locked`; keeps only shallow `pad_stack` info (layer span + drill start/end), drops drill geometry and advanced padstack settings.
|
||||||
|
- `Pad`: drops `locked`, full `pad_stack`, clearance override, die length/delay, symbol pin metadata.
|
||||||
|
- `FootprintInstance`: keeps id/ref/pos/orientation/layer/pad_count; drops definition internals, fields (`value`, `datasheet`, `description`), attributes/overrides, symbol linkage metadata.
|
||||||
|
- `BoardGraphicShape`: keeps geometry kind as string only; drops structured geometry + graphic attributes.
|
||||||
|
- `BoardText` / `BoardTextBox`: keep body text only; drop position/box, style attributes, hyperlink, lock/knockout.
|
||||||
|
- `Zone`: keeps coarse stats (type/counts/filled); drops outline, settings, border, layer properties, priority.
|
||||||
|
- `Dimension`: keeps text/layer/style string only; drops detailed unit/precision/style geometry and overrides.
|
||||||
|
- `Group`: keeps `item_count`; drops actual item id list.
|
||||||
|
|
||||||
|
## Extra Coverage Gaps
|
||||||
|
|
||||||
|
- `decode_pcb_item` supports 12 board item payload types only. Other PCB object types can appear as `Unknown` in typed API.
|
||||||
|
- `proto` module is crate-private. Consumers get `Any` bytes, not generated proto structs from this crate.
|
||||||
|
|
||||||
|
## Implementation Plan (follow in order)
|
||||||
|
|
||||||
|
### Phase 1: additive APIs, zero breakage
|
||||||
|
|
||||||
|
1. Add richer selection-return models in `src/model/common.rs`:
|
||||||
|
- `SelectionStringDump { ids: Vec<String>, contents: String }`
|
||||||
|
- `SelectionMutationResult { items: Vec<Any>, summary: SelectionSummary }` or equivalent typed struct without reducing to summary-only.
|
||||||
|
2. Add new `KiCadClient` methods in `src/client/selection.rs`:
|
||||||
|
- `get_selection_with_types(type_codes: Vec<i32>) -> Vec<PcbItem>` and raw/details variants.
|
||||||
|
- `get_selection_string_dump() -> SelectionStringDump` (keep existing `get_selection_as_string` as convenience).
|
||||||
|
- Rich mutation variants for add/remove/clear that expose returned items, not summary only.
|
||||||
|
3. Export new models via `src/lib.rs`.
|
||||||
|
4. Add blocking mirror methods in `src/blocking.rs`.
|
||||||
|
|
||||||
|
### Phase 2: reduce typed-model loss
|
||||||
|
|
||||||
|
1. Expand `Pcb*` structs in `src/model/board.rs` with additive optional fields (no removals).
|
||||||
|
2. Update `decode_pcb_item` mapping in `src/client/decode.rs` to fill new fields.
|
||||||
|
3. Prefer structured enums over stringified debug fields where possible:
|
||||||
|
- graphic geometry
|
||||||
|
- dimension style
|
||||||
|
4. Preserve backward compatibility:
|
||||||
|
- existing fields remain
|
||||||
|
- new fields optional/defaultable
|
||||||
|
|
||||||
|
### Phase 3: unhandled item kinds
|
||||||
|
|
||||||
|
1. Add typed support for additional PCB object payload types if proto types exist in generated files.
|
||||||
|
2. If unavailable in proto snapshot, keep `Unknown` fallback; include `type_url` + `raw_len`.
|
||||||
|
|
||||||
|
### Phase 4: docs/tests/regression
|
||||||
|
|
||||||
|
1. Unit tests in `src/client/tests.rs`:
|
||||||
|
- new selection filter path
|
||||||
|
- new response models keep previously dropped fields
|
||||||
|
- backward compatibility on old methods
|
||||||
|
2. Update docs:
|
||||||
|
- `README.md` API table
|
||||||
|
- `docs/PCB_SELECTION_DEEP_DUMP.md` sequence updates
|
||||||
|
3. Validation commands:
|
||||||
|
- `cargo fmt --all`
|
||||||
|
- `cargo test`
|
||||||
|
- `cargo test --features blocking`
|
||||||
|
|
||||||
|
## Decision Log Needed Before Coding
|
||||||
|
|
||||||
|
- Whether to expose proto-level structs publicly (`pub mod proto`) vs keep custom models only.
|
||||||
|
- Whether `get_selection` should stay “compact model” and new methods be “full model” (recommended).
|
||||||
|
- Naming:
|
||||||
|
- keep existing methods untouched
|
||||||
|
- add explicit `*_full`/`*_rich` APIs for clarity.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- No breaking changes in existing method signatures.
|
||||||
|
- New selection APIs expose:
|
||||||
|
- selection type filtering
|
||||||
|
- `SavedSelectionResponse.ids`
|
||||||
|
- non-summary mutation payload access
|
||||||
|
- materially more per-item structured data than current `PcbItem`.
|
||||||
|
- Existing examples still compile; add one new example showcasing rich selection extraction.
|
||||||
|
|
@ -11,13 +11,14 @@ cargo test --features blocking
|
||||||
## Evidence Pointers
|
## Evidence Pointers
|
||||||
|
|
||||||
- Unit tests across client/model/blocking/CLI parser paths:
|
- Unit tests across client/model/blocking/CLI parser paths:
|
||||||
- [`src/client.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/client.rs)
|
- [`src/client/mod.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/client/mod.rs)
|
||||||
|
- [`src/client/tests.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/client/tests.rs)
|
||||||
- [`src/blocking.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/blocking.rs)
|
- [`src/blocking.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/blocking.rs)
|
||||||
- [`src/model/common.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/model/common.rs)
|
- [`src/model/common.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/model/common.rs)
|
||||||
- [`src/model/board.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/model/board.rs)
|
- [`src/model/board.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/src/model/board.rs)
|
||||||
- [`test-scripts/kicad-ipc-cli.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/test-scripts/kicad-ipc-cli.rs)
|
- [`test-scripts/kicad-ipc-cli.rs`](https://github.com/Milind220/kicad-ipc-rs/blob/main/test-scripts/kicad-ipc-cli.rs)
|
||||||
- Runtime command coverage matrix:
|
- Runtime command coverage matrix:
|
||||||
- [README coverage section](https://github.com/Milind220/kicad-ipc-rs#kicad-v1000-api-completion-matrix)
|
- [README coverage section](https://github.com/Milind220/kicad-ipc-rs#kicad-v1000-api-reference)
|
||||||
- Runtime CLI verification flow:
|
- Runtime CLI verification flow:
|
||||||
- [docs/TEST_CLI.md](https://github.com/Milind220/kicad-ipc-rs/blob/main/docs/TEST_CLI.md)
|
- [docs/TEST_CLI.md](https://github.com/Milind220/kicad-ipc-rs/blob/main/docs/TEST_CLI.md)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,73 @@
|
||||||
|
//! Inspect the current PCB board — list nets, layers, and origin.
|
||||||
|
//!
|
||||||
|
//! Run with:
|
||||||
|
//! cargo run --example board_inspector --features blocking
|
||||||
|
|
||||||
|
#[cfg(feature = "blocking")]
|
||||||
|
use kicad_ipc_rs::{BoardOriginKind, KiCadClientBlocking};
|
||||||
|
|
||||||
|
#[cfg(feature = "blocking")]
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let client = KiCadClientBlocking::connect()?;
|
||||||
|
client.ping()?;
|
||||||
|
|
||||||
|
if !client.has_open_board()? {
|
||||||
|
eprintln!("No board is open in KiCad. Open a .kicad_pcb file first.");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Nets ──────────────────────────────────────────────
|
||||||
|
let nets = client.get_nets()?;
|
||||||
|
println!("Nets ({} total):", nets.len());
|
||||||
|
for net in nets.iter().take(20) {
|
||||||
|
println!(" [{:>3}] {}", net.code, net.name);
|
||||||
|
}
|
||||||
|
if nets.len() > 20 {
|
||||||
|
println!(" … and {} more", nets.len() - 20);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Enabled layers ────────────────────────────────────
|
||||||
|
let layers = client.get_board_enabled_layers()?;
|
||||||
|
println!(
|
||||||
|
"
|
||||||
|
Enabled layers ({} copper, {} total IDs):",
|
||||||
|
layers.copper_layer_count,
|
||||||
|
layers.layers.len()
|
||||||
|
);
|
||||||
|
for layer in layers.layers.iter().take(10) {
|
||||||
|
println!(" layer {:>2} → {}", layer.id, layer.name);
|
||||||
|
}
|
||||||
|
if layers.layers.len() > 10 {
|
||||||
|
println!(" … and {} more", layers.layers.len() - 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Board origins ─────────────────────────────────────
|
||||||
|
let grid_origin = client.get_board_origin(BoardOriginKind::Grid)?;
|
||||||
|
let drill_origin = client.get_board_origin(BoardOriginKind::Drill)?;
|
||||||
|
println!(
|
||||||
|
"
|
||||||
|
Grid origin : ({}, {}) nm",
|
||||||
|
grid_origin.x_nm, grid_origin.y_nm
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"Drill origin : ({}, {}) nm",
|
||||||
|
drill_origin.x_nm, drill_origin.y_nm
|
||||||
|
);
|
||||||
|
|
||||||
|
// ── Active layer ──────────────────────────────────────
|
||||||
|
let active = client.get_active_layer()?;
|
||||||
|
println!(
|
||||||
|
"
|
||||||
|
Active layer : {} ({})",
|
||||||
|
active.id, active.name
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "blocking"))]
|
||||||
|
fn main() {
|
||||||
|
eprintln!("This example requires the blocking feature:");
|
||||||
|
eprintln!(" cargo run --example board_inspector --features blocking");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
//! Minimal "hello world" example — connect to KiCad and print version info.
|
||||||
|
//!
|
||||||
|
//! Run with:
|
||||||
|
//! cargo run --example hello_kicad --features blocking
|
||||||
|
|
||||||
|
#[cfg(feature = "blocking")]
|
||||||
|
use kicad_ipc_rs::KiCadClientBlocking;
|
||||||
|
|
||||||
|
#[cfg(feature = "blocking")]
|
||||||
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
// Connect to a running KiCad instance.
|
||||||
|
// Auto-detects the IPC socket; override with KICAD_API_SOCKET env var.
|
||||||
|
let client = KiCadClientBlocking::connect()?;
|
||||||
|
|
||||||
|
// Health check — verifies the connection is alive.
|
||||||
|
client.ping()?;
|
||||||
|
println!("✓ Connected to KiCad");
|
||||||
|
|
||||||
|
// Retrieve version metadata.
|
||||||
|
let version = client.get_version()?;
|
||||||
|
println!(" Version : {}", version.full_version);
|
||||||
|
println!(
|
||||||
|
" SemVer : {}.{}.{}",
|
||||||
|
version.major, version.minor, version.patch
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check whether a PCB document is open.
|
||||||
|
if client.has_open_board()? {
|
||||||
|
let path = client.get_current_project_path()?;
|
||||||
|
println!(" Project : {}", path.display());
|
||||||
|
} else {
|
||||||
|
println!(" (no board open)");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(feature = "blocking"))]
|
||||||
|
fn main() {
|
||||||
|
eprintln!("This example requires the blocking feature:");
|
||||||
|
eprintln!(" cargo run --example hello_kicad --features blocking");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://randomlabs.ai/config.json",
|
||||||
|
"permission": {
|
||||||
|
"*": "allow",
|
||||||
|
"bash": "ask",
|
||||||
|
"edit": "ask"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
//! Blocking facade over the async [`KiCadClient`] API.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::mpsc::{self, SyncSender};
|
use std::sync::mpsc::{self, SyncSender};
|
||||||
|
|
@ -117,43 +119,53 @@ impl Drop for BlockingCore {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
/// Thread-safe blocking KiCad IPC client.
|
||||||
|
///
|
||||||
|
/// This wrapper runs async operations on a dedicated Tokio runtime thread.
|
||||||
pub struct KiCadClientBlocking {
|
pub struct KiCadClientBlocking {
|
||||||
inner: KiCadClient,
|
inner: KiCadClient,
|
||||||
core: Arc<BlockingCore>,
|
core: Arc<BlockingCore>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
/// Builder for [`KiCadClientBlocking`].
|
||||||
pub struct KiCadClientBlockingBuilder {
|
pub struct KiCadClientBlockingBuilder {
|
||||||
inner: ClientBuilder,
|
inner: ClientBuilder,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KiCadClientBlockingBuilder {
|
impl KiCadClientBlockingBuilder {
|
||||||
|
/// Creates a blocking client builder with default configuration.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: ClientBuilder::new(),
|
inner: ClientBuilder::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets IPC timeout used by the underlying async client.
|
||||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||||
self.inner = self.inner.timeout(timeout);
|
self.inner = self.inner.timeout(timeout);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets KiCad IPC socket path/URI.
|
||||||
pub fn socket_path(mut self, socket_path: impl Into<String>) -> Self {
|
pub fn socket_path(mut self, socket_path: impl Into<String>) -> Self {
|
||||||
self.inner = self.inner.socket_path(socket_path);
|
self.inner = self.inner.socket_path(socket_path);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets authentication token sent to KiCad IPC.
|
||||||
pub fn token(mut self, token: impl Into<String>) -> Self {
|
pub fn token(mut self, token: impl Into<String>) -> Self {
|
||||||
self.inner = self.inner.token(token);
|
self.inner = self.inner.token(token);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets client name sent during IPC handshake.
|
||||||
pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
|
pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
|
||||||
self.inner = self.inner.client_name(client_name);
|
self.inner = self.inner.client_name(client_name);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connects and returns a ready-to-use blocking client.
|
||||||
pub fn connect(self) -> Result<KiCadClientBlocking, KiCadError> {
|
pub fn connect(self) -> Result<KiCadClientBlocking, KiCadError> {
|
||||||
let core = BlockingCore::start()?;
|
let core = BlockingCore::start()?;
|
||||||
let inner_builder = self.inner;
|
let inner_builder = self.inner;
|
||||||
|
|
@ -174,6 +186,7 @@ macro_rules! blocking_methods {
|
||||||
$(fn $name:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty;)+
|
$(fn $name:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty;)+
|
||||||
) => {
|
) => {
|
||||||
$(
|
$(
|
||||||
|
#[doc = concat!("Blocking wrapper for [`KiCadClient::", stringify!($name), "`].")]
|
||||||
pub fn $name(&self, $($arg: $arg_ty),*) -> $ret {
|
pub fn $name(&self, $($arg: $arg_ty),*) -> $ret {
|
||||||
let client = self.inner.clone();
|
let client = self.inner.clone();
|
||||||
self.core.call(move |runtime| runtime.block_on(async move {
|
self.core.call(move |runtime| runtime.block_on(async move {
|
||||||
|
|
@ -190,26 +203,32 @@ macro_rules! blocking_methods {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KiCadClientBlocking {
|
impl KiCadClientBlocking {
|
||||||
|
/// Returns a builder for configuring a blocking KiCad client.
|
||||||
pub fn builder() -> KiCadClientBlockingBuilder {
|
pub fn builder() -> KiCadClientBlockingBuilder {
|
||||||
KiCadClientBlockingBuilder::new()
|
KiCadClientBlockingBuilder::new()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Connects using default blocking client configuration.
|
||||||
pub fn connect() -> Result<Self, KiCadError> {
|
pub fn connect() -> Result<Self, KiCadError> {
|
||||||
KiCadClientBlockingBuilder::new().connect()
|
KiCadClientBlockingBuilder::new().connect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns configured request timeout.
|
||||||
pub fn timeout(&self) -> Duration {
|
pub fn timeout(&self) -> Duration {
|
||||||
self.inner.timeout()
|
self.inner.timeout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns configured KiCad IPC socket URI.
|
||||||
pub fn socket_uri(&self) -> &str {
|
pub fn socket_uri(&self) -> &str {
|
||||||
self.inner.socket_uri()
|
self.inner.socket_uri()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the underlying async client reference.
|
||||||
pub fn inner(&self) -> &KiCadClient {
|
pub fn inner(&self) -> &KiCadClient {
|
||||||
&self.inner
|
&self.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a KiCad action and returns the raw action response payload.
|
||||||
pub fn run_action_raw(&self, action: impl Into<String>) -> Result<Any, KiCadError> {
|
pub fn run_action_raw(&self, action: impl Into<String>) -> Result<Any, KiCadError> {
|
||||||
let action = action.into();
|
let action = action.into();
|
||||||
let client = self.inner.clone();
|
let client = self.inner.clone();
|
||||||
|
|
@ -218,6 +237,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs a KiCad action and returns mapped status.
|
||||||
pub fn run_action(&self, action: impl Into<String>) -> Result<RunActionStatus, KiCadError> {
|
pub fn run_action(&self, action: impl Into<String>) -> Result<RunActionStatus, KiCadError> {
|
||||||
let action = action.into();
|
let action = action.into();
|
||||||
let client = self.inner.clone();
|
let client = self.inner.clone();
|
||||||
|
|
@ -225,6 +245,7 @@ impl KiCadClientBlocking {
|
||||||
.call(move |runtime| runtime.block_on(async move { client.run_action(action).await }))
|
.call(move |runtime| runtime.block_on(async move { client.run_action(action).await }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a KiCad binary path and returns raw response payload.
|
||||||
pub fn get_kicad_binary_path_raw(
|
pub fn get_kicad_binary_path_raw(
|
||||||
&self,
|
&self,
|
||||||
binary_name: impl Into<String>,
|
binary_name: impl Into<String>,
|
||||||
|
|
@ -236,6 +257,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves a KiCad binary path.
|
||||||
pub fn get_kicad_binary_path(
|
pub fn get_kicad_binary_path(
|
||||||
&self,
|
&self,
|
||||||
binary_name: impl Into<String>,
|
binary_name: impl Into<String>,
|
||||||
|
|
@ -247,6 +269,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves plugin settings path and returns raw response payload.
|
||||||
pub fn get_plugin_settings_path_raw(
|
pub fn get_plugin_settings_path_raw(
|
||||||
&self,
|
&self,
|
||||||
identifier: impl Into<String>,
|
identifier: impl Into<String>,
|
||||||
|
|
@ -258,6 +281,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves plugin settings path.
|
||||||
pub fn get_plugin_settings_path(
|
pub fn get_plugin_settings_path(
|
||||||
&self,
|
&self,
|
||||||
identifier: impl Into<String>,
|
identifier: impl Into<String>,
|
||||||
|
|
@ -269,6 +293,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ends a commit session and returns raw response payload.
|
||||||
pub fn end_commit_raw(
|
pub fn end_commit_raw(
|
||||||
&self,
|
&self,
|
||||||
session: CommitSession,
|
session: CommitSession,
|
||||||
|
|
@ -282,6 +307,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ends a commit session.
|
||||||
pub fn end_commit(
|
pub fn end_commit(
|
||||||
&self,
|
&self,
|
||||||
session: CommitSession,
|
session: CommitSession,
|
||||||
|
|
@ -295,6 +321,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses KiCad item text and creates items, returning raw response payload.
|
||||||
pub fn parse_and_create_items_from_string_raw(
|
pub fn parse_and_create_items_from_string_raw(
|
||||||
&self,
|
&self,
|
||||||
contents: impl Into<String>,
|
contents: impl Into<String>,
|
||||||
|
|
@ -310,6 +337,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parses KiCad item text and returns created items as raw payloads.
|
||||||
pub fn parse_and_create_items_from_string(
|
pub fn parse_and_create_items_from_string(
|
||||||
&self,
|
&self,
|
||||||
contents: impl Into<String>,
|
contents: impl Into<String>,
|
||||||
|
|
@ -322,6 +350,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Injects a DRC marker and returns raw response payload.
|
||||||
pub fn inject_drc_error_raw(
|
pub fn inject_drc_error_raw(
|
||||||
&self,
|
&self,
|
||||||
severity: DrcSeverity,
|
severity: DrcSeverity,
|
||||||
|
|
@ -340,6 +369,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Injects a DRC marker and returns marker id when available.
|
||||||
pub fn inject_drc_error(
|
pub fn inject_drc_error(
|
||||||
&self,
|
&self,
|
||||||
severity: DrcSeverity,
|
severity: DrcSeverity,
|
||||||
|
|
@ -358,6 +388,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves a copy of the active document and returns raw response payload.
|
||||||
pub fn save_copy_of_document_raw(
|
pub fn save_copy_of_document_raw(
|
||||||
&self,
|
&self,
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
|
|
@ -375,6 +406,7 @@ impl KiCadClientBlocking {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Saves a copy of the active document.
|
||||||
pub fn save_copy_of_document(
|
pub fn save_copy_of_document(
|
||||||
&self,
|
&self,
|
||||||
path: impl Into<String>,
|
path: impl Into<String>,
|
||||||
|
|
@ -586,7 +618,18 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn sync_wrapper_covers_async_method_names() {
|
fn sync_wrapper_covers_async_method_names() {
|
||||||
let mut async_methods = BTreeSet::new();
|
let mut async_methods = BTreeSet::new();
|
||||||
for line in include_str!("client.rs").lines() {
|
let source = [
|
||||||
|
include_str!("client/mod.rs"),
|
||||||
|
include_str!("client/common.rs"),
|
||||||
|
include_str!("client/board.rs"),
|
||||||
|
include_str!("client/selection.rs"),
|
||||||
|
include_str!("client/items.rs"),
|
||||||
|
include_str!("client/document.rs"),
|
||||||
|
include_str!("client/geometry.rs"),
|
||||||
|
]
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
for line in source.lines() {
|
||||||
let trimmed = line.trim_start();
|
let trimmed = line.trim_start();
|
||||||
if let Some(rest) = trimmed.strip_prefix("pub async fn ") {
|
if let Some(rest) = trimmed.strip_prefix("pub async fn ") {
|
||||||
if let Some(name) = rest.split('(').next() {
|
if let Some(name) = rest.split('(').next() {
|
||||||
|
|
@ -594,7 +637,6 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let blocking_methods: BTreeSet<String> =
|
let blocking_methods: BTreeSet<String> =
|
||||||
KiCadClientBlocking::GENERATED_BLOCKING_METHOD_NAMES
|
KiCadClientBlocking::GENERATED_BLOCKING_METHOD_NAMES
|
||||||
.iter()
|
.iter()
|
||||||
|
|
|
||||||
5448
src/client.rs
5448
src/client.rs
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,381 @@
|
||||||
|
//! Board-specific operations: nets, layers, origin, stackup, graphics defaults, and DRC.
|
||||||
|
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::proto::kiapi::board::commands as board_commands;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
|
||||||
|
use super::mappers::*;
|
||||||
|
use super::{
|
||||||
|
KiCadClient, CMD_GET_ACTIVE_LAYER, CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS,
|
||||||
|
CMD_GET_BOARD_ENABLED_LAYERS, CMD_GET_BOARD_LAYER_NAME, CMD_GET_BOARD_ORIGIN,
|
||||||
|
CMD_GET_BOARD_STACKUP, CMD_GET_GRAPHICS_DEFAULTS, CMD_GET_NETS, CMD_GET_VISIBLE_LAYERS,
|
||||||
|
CMD_INJECT_DRC_ERROR, CMD_INTERACTIVE_MOVE_ITEMS, CMD_SET_ACTIVE_LAYER,
|
||||||
|
CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS, CMD_SET_BOARD_ENABLED_LAYERS, CMD_SET_BOARD_ORIGIN,
|
||||||
|
CMD_SET_VISIBLE_LAYERS, CMD_UPDATE_BOARD_STACKUP, RES_BOARD_EDITOR_APPEARANCE_SETTINGS,
|
||||||
|
RES_BOARD_LAYERS, RES_BOARD_LAYER_NAME_RESPONSE, RES_BOARD_LAYER_RESPONSE,
|
||||||
|
RES_BOARD_STACKUP_RESPONSE, RES_GET_BOARD_ENABLED_LAYERS, RES_GET_NETS,
|
||||||
|
RES_GRAPHICS_DEFAULTS_RESPONSE, RES_INJECT_DRC_ERROR_RESPONSE, RES_PROTOBUF_EMPTY, RES_VECTOR2,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Lists nets in the active PCB document.
|
||||||
|
pub async fn get_nets(&self) -> Result<Vec<BoardNet>, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::GetNets {
|
||||||
|
board: Some(board),
|
||||||
|
netclass_filter: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_NETS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: board_commands::NetsResponse = envelope::unpack_any(&response, RES_GET_NETS)?;
|
||||||
|
|
||||||
|
Ok(payload
|
||||||
|
.nets
|
||||||
|
.into_iter()
|
||||||
|
.map(|net| BoardNet {
|
||||||
|
code: net.code.map_or(0, |code| code.value),
|
||||||
|
name: net.name,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns enabled board layers and current copper layer count.
|
||||||
|
pub async fn get_board_enabled_layers(&self) -> Result<BoardEnabledLayers, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::GetBoardEnabledLayers { board: Some(board) };
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_BOARD_ENABLED_LAYERS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: board_commands::BoardEnabledLayersResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_BOARD_ENABLED_LAYERS)?;
|
||||||
|
|
||||||
|
Ok(map_board_enabled_layers_response(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets enabled layers and copper layer count, then returns resulting state.
|
||||||
|
pub async fn set_board_enabled_layers(
|
||||||
|
&self,
|
||||||
|
copper_layer_count: u32,
|
||||||
|
layer_ids: Vec<i32>,
|
||||||
|
) -> Result<BoardEnabledLayers, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::SetBoardEnabledLayers {
|
||||||
|
board: Some(board),
|
||||||
|
copper_layer_count,
|
||||||
|
layers: layer_ids,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SET_BOARD_ENABLED_LAYERS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: board_commands::BoardEnabledLayersResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_BOARD_ENABLED_LAYERS)?;
|
||||||
|
Ok(map_board_enabled_layers_response(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the currently active drawing layer.
|
||||||
|
pub async fn get_active_layer(&self) -> Result<BoardLayerInfo, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::GetActiveLayer { board: Some(board) };
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ACTIVE_LAYER))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: board_commands::BoardLayerResponse =
|
||||||
|
envelope::unpack_any(&response, RES_BOARD_LAYER_RESPONSE)?;
|
||||||
|
|
||||||
|
Ok(layer_to_model(payload.layer))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the active drawing layer by KiCad layer id.
|
||||||
|
pub async fn set_active_layer(&self, layer_id: i32) -> Result<(), KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::SetActiveLayer {
|
||||||
|
board: Some(board),
|
||||||
|
layer: layer_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_command(envelope::pack_any(&command, CMD_SET_ACTIVE_LAYER))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all currently visible layers.
|
||||||
|
pub async fn get_visible_layers(&self) -> Result<Vec<BoardLayerInfo>, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::GetVisibleLayers { board: Some(board) };
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_VISIBLE_LAYERS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: board_commands::BoardLayers =
|
||||||
|
envelope::unpack_any(&response, RES_BOARD_LAYERS)?;
|
||||||
|
|
||||||
|
Ok(payload.layers.into_iter().map(layer_to_model).collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets visible layers by KiCad layer ids.
|
||||||
|
pub async fn set_visible_layers(&self, layer_ids: Vec<i32>) -> Result<(), KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::SetVisibleLayers {
|
||||||
|
board: Some(board),
|
||||||
|
layers: layer_ids,
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_command(envelope::pack_any(&command, CMD_SET_VISIBLE_LAYERS))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a layer id to its display name.
|
||||||
|
pub async fn get_board_layer_name(&self, layer_id: i32) -> Result<String, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::GetBoardLayerName {
|
||||||
|
board: Some(board),
|
||||||
|
layer: layer_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_BOARD_LAYER_NAME))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: board_commands::BoardLayerNameResponse =
|
||||||
|
envelope::unpack_any(&response, RES_BOARD_LAYER_NAME_RESPONSE)?;
|
||||||
|
Ok(payload.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the board origin for the requested origin kind.
|
||||||
|
pub async fn get_board_origin(&self, kind: BoardOriginKind) -> Result<Vector2Nm, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::GetBoardOrigin {
|
||||||
|
board: Some(board),
|
||||||
|
r#type: board_origin_kind_to_proto(kind),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_BOARD_ORIGIN))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_types::Vector2 = envelope::unpack_any(&response, RES_VECTOR2)?;
|
||||||
|
Ok(Vector2Nm {
|
||||||
|
x_nm: payload.x_nm,
|
||||||
|
y_nm: payload.y_nm,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the board origin for the requested origin kind.
|
||||||
|
pub async fn set_board_origin(
|
||||||
|
&self,
|
||||||
|
kind: BoardOriginKind,
|
||||||
|
origin: Vector2Nm,
|
||||||
|
) -> Result<(), KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::SetBoardOrigin {
|
||||||
|
board: Some(board),
|
||||||
|
r#type: board_origin_kind_to_proto(kind),
|
||||||
|
origin: Some(vector2_nm_to_proto(origin)),
|
||||||
|
};
|
||||||
|
|
||||||
|
self.send_command(envelope::pack_any(&command, CMD_SET_BOARD_ORIGIN))
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injects a DRC marker in the active board and returns raw response payload.
|
||||||
|
pub async fn inject_drc_error_raw(
|
||||||
|
&self,
|
||||||
|
severity: DrcSeverity,
|
||||||
|
message: impl Into<String>,
|
||||||
|
position: Option<Vector2Nm>,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::InjectDrcError {
|
||||||
|
board: Some(board),
|
||||||
|
severity: drc_severity_to_proto(severity),
|
||||||
|
message: message.into(),
|
||||||
|
position: position.map(vector2_nm_to_proto),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_INJECT_DRC_ERROR))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_INJECT_DRC_ERROR_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Injects a DRC marker and returns the created marker id when available.
|
||||||
|
pub async fn inject_drc_error(
|
||||||
|
&self,
|
||||||
|
severity: DrcSeverity,
|
||||||
|
message: impl Into<String>,
|
||||||
|
position: Option<Vector2Nm>,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Option<String>, KiCadError> {
|
||||||
|
let payload = self
|
||||||
|
.inject_drc_error_raw(severity, message, position, item_ids)
|
||||||
|
.await?;
|
||||||
|
let response: board_commands::InjectDrcErrorResponse =
|
||||||
|
decode_any(&payload, RES_INJECT_DRC_ERROR_RESPONSE)?;
|
||||||
|
Ok(response.marker.map(|marker| marker.value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns board stackup response as raw protobuf payload.
|
||||||
|
pub async fn get_board_stackup_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = board_commands::GetBoardStackup {
|
||||||
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_BOARD_STACKUP))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads board stackup from the active PCB document.
|
||||||
|
pub async fn get_board_stackup(&self) -> Result<BoardStackup, KiCadError> {
|
||||||
|
let payload = self.get_board_stackup_raw().await?;
|
||||||
|
let response: board_commands::BoardStackupResponse =
|
||||||
|
decode_any(&payload, RES_BOARD_STACKUP_RESPONSE)?;
|
||||||
|
Ok(map_board_stackup(response.stackup.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sends a stackup update and returns the raw protobuf response payload.
|
||||||
|
pub async fn update_board_stackup_raw(
|
||||||
|
&self,
|
||||||
|
stackup: BoardStackup,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = board_commands::UpdateBoardStackup {
|
||||||
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
stackup: Some(board_stackup_to_proto(stackup)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_UPDATE_BOARD_STACKUP))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response_payload_as_any(response, RES_BOARD_STACKUP_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes a board stackup and returns KiCad's resulting stackup state.
|
||||||
|
pub async fn update_board_stackup(
|
||||||
|
&self,
|
||||||
|
stackup: BoardStackup,
|
||||||
|
) -> Result<BoardStackup, KiCadError> {
|
||||||
|
let payload = self.update_board_stackup_raw(stackup).await?;
|
||||||
|
let response: board_commands::BoardStackupResponse =
|
||||||
|
decode_any(&payload, RES_BOARD_STACKUP_RESPONSE)?;
|
||||||
|
Ok(map_board_stackup(response.stackup.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns graphics defaults as raw protobuf payload.
|
||||||
|
pub async fn get_graphics_defaults_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = board_commands::GetGraphicsDefaults {
|
||||||
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_GRAPHICS_DEFAULTS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response_payload_as_any(response, RES_GRAPHICS_DEFAULTS_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mapped board graphics defaults.
|
||||||
|
pub async fn get_graphics_defaults(&self) -> Result<GraphicsDefaults, KiCadError> {
|
||||||
|
let payload = self.get_graphics_defaults_raw().await?;
|
||||||
|
let response: board_commands::GraphicsDefaultsResponse =
|
||||||
|
decode_any(&payload, RES_GRAPHICS_DEFAULTS_RESPONSE)?;
|
||||||
|
Ok(map_graphics_defaults(response.defaults.unwrap_or_default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns editor appearance settings as raw protobuf payload.
|
||||||
|
pub async fn get_board_editor_appearance_settings_raw(
|
||||||
|
&self,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = board_commands::GetBoardEditorAppearanceSettings {};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(
|
||||||
|
&command,
|
||||||
|
CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response_payload_as_any(response, RES_BOARD_EDITOR_APPEARANCE_SETTINGS)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mapped board editor appearance settings.
|
||||||
|
pub async fn get_board_editor_appearance_settings(
|
||||||
|
&self,
|
||||||
|
) -> Result<BoardEditorAppearanceSettings, KiCadError> {
|
||||||
|
let payload = self.get_board_editor_appearance_settings_raw().await?;
|
||||||
|
let response: board_commands::BoardEditorAppearanceSettings =
|
||||||
|
decode_any(&payload, RES_BOARD_EDITOR_APPEARANCE_SETTINGS)?;
|
||||||
|
Ok(map_board_editor_appearance_settings(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets board editor appearance settings and returns resulting persisted settings.
|
||||||
|
pub async fn set_board_editor_appearance_settings(
|
||||||
|
&self,
|
||||||
|
settings: BoardEditorAppearanceSettings,
|
||||||
|
) -> Result<BoardEditorAppearanceSettings, KiCadError> {
|
||||||
|
let command = board_commands::SetBoardEditorAppearanceSettings {
|
||||||
|
settings: Some(board_editor_appearance_settings_to_proto(settings)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(
|
||||||
|
&command,
|
||||||
|
CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
let _ = response_payload_as_any(response, RES_PROTOBUF_EMPTY)?;
|
||||||
|
self.get_board_editor_appearance_settings().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts an interactive move for the provided items and returns raw response payload.
|
||||||
|
pub async fn interactive_move_items_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Err(KiCadError::Config {
|
||||||
|
reason: "interactive_move_items_raw requires at least one item id".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = board_commands::InteractiveMoveItems {
|
||||||
|
board: Some(self.current_board_document_proto().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_INTERACTIVE_MOVE_ITEMS))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_PROTOBUF_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts an interactive move for the provided items.
|
||||||
|
pub async fn interactive_move_items(&self, item_ids: Vec<String>) -> Result<(), KiCadError> {
|
||||||
|
let _ = self.interactive_move_items_raw(item_ids).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,350 @@
|
||||||
|
//! Common API operations: version, paths, documents, text variables, and text geometry.
|
||||||
|
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::model::common::*;
|
||||||
|
use crate::proto::kiapi::common::commands as common_commands;
|
||||||
|
use crate::proto::kiapi::common::project as common_project;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::mappers::*;
|
||||||
|
use super::{
|
||||||
|
map_document_specifier, project_document_proto, resolve_current_project_path, rpc, KiCadClient,
|
||||||
|
CMD_EXPAND_TEXT_VARIABLES, CMD_GET_KICAD_BINARY_PATH, CMD_GET_NET_CLASSES,
|
||||||
|
CMD_GET_OPEN_DOCUMENTS, CMD_GET_PLUGIN_SETTINGS_PATH, CMD_GET_TEXT_AS_SHAPES,
|
||||||
|
CMD_GET_TEXT_EXTENTS, CMD_GET_TEXT_VARIABLES, CMD_GET_VERSION, CMD_PING, CMD_REFRESH_EDITOR,
|
||||||
|
CMD_RUN_ACTION, CMD_SET_NET_CLASSES, CMD_SET_TEXT_VARIABLES, RES_BOX2,
|
||||||
|
RES_EXPAND_TEXT_VARIABLES_RESPONSE, RES_GET_OPEN_DOCUMENTS, RES_GET_TEXT_AS_SHAPES_RESPONSE,
|
||||||
|
RES_GET_VERSION, RES_NET_CLASSES_RESPONSE, RES_PATH_RESPONSE, RES_PROTOBUF_EMPTY,
|
||||||
|
RES_RUN_ACTION_RESPONSE, RES_STRING_RESPONSE, RES_TEXT_VARIABLES,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Verifies IPC connectivity with a lightweight ping.
|
||||||
|
pub async fn ping(&self) -> Result<(), KiCadError> {
|
||||||
|
let command = envelope::pack_any(&common_commands::Ping {}, CMD_PING);
|
||||||
|
self.send_command(command).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requests KiCad to refresh a specific editor frame.
|
||||||
|
pub async fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError> {
|
||||||
|
let command = envelope::pack_any(
|
||||||
|
&common_commands::RefreshEditor {
|
||||||
|
frame: frame.to_proto(),
|
||||||
|
},
|
||||||
|
CMD_REFRESH_EDITOR,
|
||||||
|
);
|
||||||
|
self.send_command(command).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs a KiCad action and returns the raw action response payload.
|
||||||
|
pub async fn run_action_raw(
|
||||||
|
&self,
|
||||||
|
action: impl Into<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::RunAction {
|
||||||
|
action: action.into(),
|
||||||
|
};
|
||||||
|
rpc!(self, CMD_RUN_ACTION, command, RES_RUN_ACTION_RESPONSE)
|
||||||
|
}
|
||||||
|
/// Runs a KiCad action by action name and returns mapped status.
|
||||||
|
pub async fn run_action(
|
||||||
|
&self,
|
||||||
|
action: impl Into<String>,
|
||||||
|
) -> Result<RunActionStatus, KiCadError> {
|
||||||
|
let payload = self.run_action_raw(action).await?;
|
||||||
|
let response: common_commands::RunActionResponse =
|
||||||
|
decode_any(&payload, RES_RUN_ACTION_RESPONSE)?;
|
||||||
|
Ok(map_run_action_status(response.status))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Queries KiCad version info for the connected instance.
|
||||||
|
pub async fn get_version(&self) -> Result<VersionInfo, KiCadError> {
|
||||||
|
let command = envelope::pack_any(&common_commands::GetVersion {}, CMD_GET_VERSION);
|
||||||
|
let response = self.send_command(command).await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetVersionResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_VERSION)?;
|
||||||
|
|
||||||
|
let version = payload.version.ok_or_else(|| KiCadError::MissingPayload {
|
||||||
|
expected_type_url: "kiapi.common.types.KiCadVersion".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(VersionInfo {
|
||||||
|
major: version.major,
|
||||||
|
minor: version.minor,
|
||||||
|
patch: version.patch,
|
||||||
|
full_version: version.full_version,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a KiCad binary path and returns the raw path response payload.
|
||||||
|
pub async fn get_kicad_binary_path_raw(
|
||||||
|
&self,
|
||||||
|
binary_name: impl Into<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::GetKiCadBinaryPath {
|
||||||
|
binary_name: binary_name.into(),
|
||||||
|
};
|
||||||
|
rpc!(self, CMD_GET_KICAD_BINARY_PATH, command, RES_PATH_RESPONSE)
|
||||||
|
}
|
||||||
|
/// Resolves a KiCad binary path by binary name.
|
||||||
|
pub async fn get_kicad_binary_path(
|
||||||
|
&self,
|
||||||
|
binary_name: impl Into<String>,
|
||||||
|
) -> Result<String, KiCadError> {
|
||||||
|
let payload = self.get_kicad_binary_path_raw(binary_name).await?;
|
||||||
|
let response: common_commands::PathResponse = decode_any(&payload, RES_PATH_RESPONSE)?;
|
||||||
|
Ok(response.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves plugin settings path and returns the raw string response payload.
|
||||||
|
pub async fn get_plugin_settings_path_raw(
|
||||||
|
&self,
|
||||||
|
identifier: impl Into<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::GetPluginSettingsPath {
|
||||||
|
identifier: identifier.into(),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_PLUGIN_SETTINGS_PATH))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_STRING_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves plugin settings path for a plugin identifier.
|
||||||
|
pub async fn get_plugin_settings_path(
|
||||||
|
&self,
|
||||||
|
identifier: impl Into<String>,
|
||||||
|
) -> Result<String, KiCadError> {
|
||||||
|
let payload = self.get_plugin_settings_path_raw(identifier).await?;
|
||||||
|
let response: common_commands::StringResponse = decode_any(&payload, RES_STRING_RESPONSE)?;
|
||||||
|
Ok(response.response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lists open KiCad documents of the requested type.
|
||||||
|
pub async fn get_open_documents(
|
||||||
|
&self,
|
||||||
|
document_type: DocumentType,
|
||||||
|
) -> Result<Vec<DocumentSpecifier>, KiCadError> {
|
||||||
|
let command = common_commands::GetOpenDocuments {
|
||||||
|
r#type: document_type.to_proto(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_OPEN_DOCUMENTS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetOpenDocumentsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_OPEN_DOCUMENTS)?;
|
||||||
|
|
||||||
|
Ok(payload
|
||||||
|
.documents
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(map_document_specifier)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns project net classes as raw protobuf payload.
|
||||||
|
pub async fn get_net_classes_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::GetNetClasses {};
|
||||||
|
rpc!(self, CMD_GET_NET_CLASSES, command, RES_NET_CLASSES_RESPONSE)
|
||||||
|
}
|
||||||
|
/// Reads project net classes from the current project context.
|
||||||
|
pub async fn get_net_classes(&self) -> Result<Vec<NetClassInfo>, KiCadError> {
|
||||||
|
let payload = self.get_net_classes_raw().await?;
|
||||||
|
let response: common_commands::NetClassesResponse =
|
||||||
|
decode_any(&payload, RES_NET_CLASSES_RESPONSE)?;
|
||||||
|
|
||||||
|
let mut classes: Vec<NetClassInfo> = response
|
||||||
|
.net_classes
|
||||||
|
.into_iter()
|
||||||
|
.map(map_net_class_info)
|
||||||
|
.collect();
|
||||||
|
classes.sort_by(|left, right| left.name.cmp(&right.name));
|
||||||
|
Ok(classes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets project net classes and returns the raw operation response payload.
|
||||||
|
pub async fn set_net_classes_raw(
|
||||||
|
&self,
|
||||||
|
net_classes: Vec<NetClassInfo>,
|
||||||
|
merge_mode: MapMergeMode,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::SetNetClasses {
|
||||||
|
net_classes: net_classes
|
||||||
|
.into_iter()
|
||||||
|
.map(net_class_info_to_proto)
|
||||||
|
.collect(),
|
||||||
|
merge_mode: map_merge_mode_to_proto(merge_mode),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SET_NET_CLASSES))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_PROTOBUF_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces or merges project net classes, then returns current classes.
|
||||||
|
pub async fn set_net_classes(
|
||||||
|
&self,
|
||||||
|
net_classes: Vec<NetClassInfo>,
|
||||||
|
merge_mode: MapMergeMode,
|
||||||
|
) -> Result<Vec<NetClassInfo>, KiCadError> {
|
||||||
|
let _ = self.set_net_classes_raw(net_classes, merge_mode).await?;
|
||||||
|
self.get_net_classes().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns project text variables as raw protobuf payload.
|
||||||
|
pub async fn get_text_variables_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::GetTextVariables {
|
||||||
|
document: Some(project_document_proto()),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_TEXT_VARIABLES))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_TEXT_VARIABLES)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads project text variables.
|
||||||
|
pub async fn get_text_variables(&self) -> Result<BTreeMap<String, String>, KiCadError> {
|
||||||
|
let payload = self.get_text_variables_raw().await?;
|
||||||
|
let response: common_project::TextVariables = decode_any(&payload, RES_TEXT_VARIABLES)?;
|
||||||
|
Ok(response.variables.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets project text variables and returns the raw operation response payload.
|
||||||
|
pub async fn set_text_variables_raw(
|
||||||
|
&self,
|
||||||
|
variables: BTreeMap<String, String>,
|
||||||
|
merge_mode: MapMergeMode,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::SetTextVariables {
|
||||||
|
document: Some(project_document_proto()),
|
||||||
|
variables: Some(common_project::TextVariables {
|
||||||
|
variables: variables.into_iter().collect(),
|
||||||
|
}),
|
||||||
|
merge_mode: map_merge_mode_to_proto(merge_mode),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SET_TEXT_VARIABLES))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_PROTOBUF_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replaces or merges project text variables, then returns current values.
|
||||||
|
pub async fn set_text_variables(
|
||||||
|
&self,
|
||||||
|
variables: BTreeMap<String, String>,
|
||||||
|
merge_mode: MapMergeMode,
|
||||||
|
) -> Result<BTreeMap<String, String>, KiCadError> {
|
||||||
|
let _ = self.set_text_variables_raw(variables, merge_mode).await?;
|
||||||
|
self.get_text_variables().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands project text variables and returns the raw expansion response payload.
|
||||||
|
pub async fn expand_text_variables_raw(
|
||||||
|
&self,
|
||||||
|
text: Vec<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::ExpandTextVariables {
|
||||||
|
document: Some(project_document_proto()),
|
||||||
|
text,
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_EXPAND_TEXT_VARIABLES))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_EXPAND_TEXT_VARIABLES_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Expands `${VAR}`-style text variables using current project context.
|
||||||
|
pub async fn expand_text_variables(
|
||||||
|
&self,
|
||||||
|
text: Vec<String>,
|
||||||
|
) -> Result<Vec<String>, KiCadError> {
|
||||||
|
let payload = self.expand_text_variables_raw(text).await?;
|
||||||
|
let response: common_commands::ExpandTextVariablesResponse =
|
||||||
|
decode_any(&payload, RES_EXPAND_TEXT_VARIABLES_RESPONSE)?;
|
||||||
|
Ok(response.text)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes text extents and returns the raw bounding box payload.
|
||||||
|
pub async fn get_text_extents_raw(
|
||||||
|
&self,
|
||||||
|
text: TextSpec,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::GetTextExtents {
|
||||||
|
text: Some(text_spec_to_proto(text)),
|
||||||
|
};
|
||||||
|
rpc!(self, CMD_GET_TEXT_EXTENTS, command, RES_BOX2)
|
||||||
|
}
|
||||||
|
/// Computes rendered text extents in nanometer units.
|
||||||
|
pub async fn get_text_extents(&self, text: TextSpec) -> Result<TextExtents, KiCadError> {
|
||||||
|
let payload = self.get_text_extents_raw(text).await?;
|
||||||
|
let response: common_types::Box2 = decode_any(&payload, RES_BOX2)?;
|
||||||
|
let position = response
|
||||||
|
.position
|
||||||
|
.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "GetTextExtents response missing position".to_string(),
|
||||||
|
})?;
|
||||||
|
let size = response.size.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "GetTextExtents response missing size".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(TextExtents {
|
||||||
|
x_nm: position.x_nm,
|
||||||
|
y_nm: position.y_nm,
|
||||||
|
width_nm: size.x_nm,
|
||||||
|
height_nm: size.y_nm,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts text objects to shapes and returns the raw response payload.
|
||||||
|
pub async fn get_text_as_shapes_raw(
|
||||||
|
&self,
|
||||||
|
text: Vec<TextObjectSpec>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::GetTextAsShapes {
|
||||||
|
text: text.into_iter().map(text_object_spec_to_proto).collect(),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_TEXT_AS_SHAPES))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_GET_TEXT_AS_SHAPES_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts text/textbox specs into drawable shape geometry.
|
||||||
|
pub async fn get_text_as_shapes(
|
||||||
|
&self,
|
||||||
|
text: Vec<TextObjectSpec>,
|
||||||
|
) -> Result<Vec<TextAsShapesEntry>, KiCadError> {
|
||||||
|
let payload = self.get_text_as_shapes_raw(text).await?;
|
||||||
|
let response: common_commands::GetTextAsShapesResponse =
|
||||||
|
decode_any(&payload, RES_GET_TEXT_AS_SHAPES_RESPONSE)?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.text_with_shapes
|
||||||
|
.into_iter()
|
||||||
|
.map(map_text_with_shapes)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current project path.
|
||||||
|
///
|
||||||
|
/// First queries open PCB documents. If KiCad reports `GetOpenDocuments` as unhandled,
|
||||||
|
/// this falls back to the `KIPRJMOD` environment variable when available.
|
||||||
|
pub async fn get_current_project_path(&self) -> Result<PathBuf, KiCadError> {
|
||||||
|
let docs = self.get_open_documents(DocumentType::Pcb).await;
|
||||||
|
resolve_current_project_path(docs)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when at least one PCB document is open in KiCad.
|
||||||
|
pub async fn has_open_board(&self) -> Result<bool, KiCadError> {
|
||||||
|
let docs = self.get_open_documents(DocumentType::Pcb).await?;
|
||||||
|
Ok(!docs.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,543 @@
|
||||||
|
//! PCB item decoding from raw protobuf `Any` payloads into typed `PcbItem` variants.
|
||||||
|
|
||||||
|
use super::mappers::*;
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::proto::kiapi::board::types as board_types;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
pub(crate) 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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) 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),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_pad_type(value: i32) -> PcbPadType {
|
||||||
|
match board_types::PadType::try_from(value) {
|
||||||
|
Ok(board_types::PadType::PtPth) => PcbPadType::Pth,
|
||||||
|
Ok(board_types::PadType::PtSmd) => PcbPadType::Smd,
|
||||||
|
Ok(board_types::PadType::PtEdgeConnector) => PcbPadType::EdgeConnector,
|
||||||
|
Ok(board_types::PadType::PtNpth) => PcbPadType::Npth,
|
||||||
|
_ => PcbPadType::Unknown(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_zone_type(value: i32) -> PcbZoneType {
|
||||||
|
match board_types::ZoneType::try_from(value) {
|
||||||
|
Ok(board_types::ZoneType::ZtCopper) => PcbZoneType::Copper,
|
||||||
|
Ok(board_types::ZoneType::ZtGraphical) => PcbZoneType::Graphical,
|
||||||
|
Ok(board_types::ZoneType::ZtRuleArea) => PcbZoneType::RuleArea,
|
||||||
|
Ok(board_types::ZoneType::ZtTeardrop) => PcbZoneType::Teardrop,
|
||||||
|
_ => PcbZoneType::Unknown(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode_pcb_items(items: Vec<prost_types::Any>) -> Result<Vec<PcbItem>, KiCadError> {
|
||||||
|
items.into_iter().map(decode_pcb_item).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn decode_pcb_item(item: prost_types::Any) -> Result<PcbItem, KiCadError> {
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Track") {
|
||||||
|
let track = decode_any::<board_types::Track>(&item, "kiapi.board.types.Track")?;
|
||||||
|
return Ok(PcbItem::Track(PcbTrack {
|
||||||
|
id: track.id.map(|id| id.value),
|
||||||
|
start_nm: track.start.map(map_vector2_nm),
|
||||||
|
end_nm: track.end.map(map_vector2_nm),
|
||||||
|
width_nm: map_optional_distance_nm(track.width),
|
||||||
|
locked: map_lock_state(track.locked),
|
||||||
|
layer: layer_to_model(track.layer),
|
||||||
|
net: map_optional_net(track.net),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Arc") {
|
||||||
|
let arc = decode_any::<board_types::Arc>(&item, "kiapi.board.types.Arc")?;
|
||||||
|
return Ok(PcbItem::Arc(PcbArc {
|
||||||
|
id: arc.id.map(|id| id.value),
|
||||||
|
start_nm: arc.start.map(map_vector2_nm),
|
||||||
|
mid_nm: arc.mid.map(map_vector2_nm),
|
||||||
|
end_nm: arc.end.map(map_vector2_nm),
|
||||||
|
width_nm: map_optional_distance_nm(arc.width),
|
||||||
|
locked: map_lock_state(arc.locked),
|
||||||
|
layer: layer_to_model(arc.layer),
|
||||||
|
net: map_optional_net(arc.net),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Via") {
|
||||||
|
let via = decode_any::<board_types::Via>(&item, "kiapi.board.types.Via")?;
|
||||||
|
return Ok(PcbItem::Via(PcbVia {
|
||||||
|
id: via.id.map(|id| id.value),
|
||||||
|
position_nm: via.position.map(map_vector2_nm),
|
||||||
|
via_type: map_via_type(via.r#type),
|
||||||
|
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),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||||
|
let footprint = decode_any::<board_types::FootprintInstance>(
|
||||||
|
&item,
|
||||||
|
"kiapi.board.types.FootprintInstance",
|
||||||
|
)?;
|
||||||
|
let reference = footprint
|
||||||
|
.reference_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 value = footprint
|
||||||
|
.value_field
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|field| field.text.as_ref())
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
let datasheet = footprint
|
||||||
|
.datasheet_field
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|field| field.text.as_ref())
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
let description = footprint
|
||||||
|
.description_field
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|field| field.text.as_ref())
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.filter(|value| !value.is_empty());
|
||||||
|
let definition_item_count = footprint
|
||||||
|
.definition
|
||||||
|
.as_ref()
|
||||||
|
.map(|definition| definition.items.len())
|
||||||
|
.unwrap_or(0);
|
||||||
|
let pad_count = footprint
|
||||||
|
.definition
|
||||||
|
.as_ref()
|
||||||
|
.map(|definition| {
|
||||||
|
definition
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.type_url == envelope::type_url("kiapi.board.types.Pad"))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
let symbol_sheet_name = (!footprint.symbol_sheet_name.is_empty())
|
||||||
|
.then_some(footprint.symbol_sheet_name.clone());
|
||||||
|
let symbol_sheet_filename = (!footprint.symbol_sheet_filename.is_empty())
|
||||||
|
.then_some(footprint.symbol_sheet_filename.clone());
|
||||||
|
let symbol_footprint_filters = (!footprint.symbol_footprint_filters.is_empty())
|
||||||
|
.then_some(footprint.symbol_footprint_filters.clone());
|
||||||
|
let has_symbol_path = footprint.symbol_path.is_some();
|
||||||
|
let symbol_link = if has_symbol_path
|
||||||
|
|| symbol_sheet_name.is_some()
|
||||||
|
|| symbol_sheet_filename.is_some()
|
||||||
|
|| symbol_footprint_filters.is_some()
|
||||||
|
{
|
||||||
|
Some(PcbFootprintSymbolLink {
|
||||||
|
has_symbol_path,
|
||||||
|
sheet_name: symbol_sheet_name,
|
||||||
|
sheet_filename: symbol_sheet_filename,
|
||||||
|
footprint_filters: symbol_footprint_filters,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(PcbItem::Footprint(PcbFootprint {
|
||||||
|
id: footprint.id.map(|id| id.value),
|
||||||
|
reference,
|
||||||
|
position_nm: footprint.position.map(map_vector2_nm),
|
||||||
|
orientation_deg: footprint.orientation.map(|angle| angle.value_degrees),
|
||||||
|
layer: layer_to_model(footprint.layer),
|
||||||
|
locked: map_lock_state(footprint.locked),
|
||||||
|
value,
|
||||||
|
datasheet,
|
||||||
|
description,
|
||||||
|
has_attributes: footprint.attributes.is_some(),
|
||||||
|
has_overrides: footprint.overrides.is_some(),
|
||||||
|
has_definition: footprint.definition.is_some(),
|
||||||
|
definition_item_count,
|
||||||
|
symbol_link,
|
||||||
|
pad_count,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
|
||||||
|
let pad = decode_any::<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 {
|
||||||
|
id: pad.id.map(|id| id.value),
|
||||||
|
locked: map_lock_state(pad.locked),
|
||||||
|
number: pad.number,
|
||||||
|
pad_type: map_pad_type(pad.r#type),
|
||||||
|
position_nm: pad.position.map(map_vector2_nm),
|
||||||
|
pad_stack: map_pad_stack(pad.pad_stack.as_ref()),
|
||||||
|
copper_clearance_override_nm: map_optional_distance_nm(pad.copper_clearance_override),
|
||||||
|
pad_to_die_length_nm: map_optional_distance_nm(pad.pad_to_die_length),
|
||||||
|
pad_to_die_delay_as: pad.pad_to_die_delay.map(|value| value.value_as),
|
||||||
|
symbol_pin,
|
||||||
|
net: map_optional_net(pad.net),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
|
||||||
|
let shape = decode_any::<board_types::BoardGraphicShape>(
|
||||||
|
&item,
|
||||||
|
"kiapi.board.types.BoardGraphicShape",
|
||||||
|
)?;
|
||||||
|
let geometry_kind = map_graphic_shape_kind(shape.shape.as_ref());
|
||||||
|
let geometry = map_graphic_shape_geometry(shape.shape.as_ref());
|
||||||
|
let stroke_width_nm = shape
|
||||||
|
.shape
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|graphic| graphic.attributes.as_ref())
|
||||||
|
.and_then(|attrs| attrs.stroke.as_ref())
|
||||||
|
.and_then(|stroke| stroke.width)
|
||||||
|
.map(|width| width.value_nm);
|
||||||
|
let stroke_style = shape
|
||||||
|
.shape
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|graphic| graphic.attributes.as_ref())
|
||||||
|
.and_then(|attrs| attrs.stroke.as_ref())
|
||||||
|
.map(|stroke| {
|
||||||
|
common_types::StrokeLineStyle::try_from(stroke.style)
|
||||||
|
.map(|value| value.as_str_name().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("UNKNOWN({})", stroke.style))
|
||||||
|
});
|
||||||
|
let fill_type = shape
|
||||||
|
.shape
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|graphic| graphic.attributes.as_ref())
|
||||||
|
.and_then(|attrs| attrs.fill.as_ref())
|
||||||
|
.map(|fill| {
|
||||||
|
common_types::GraphicFillType::try_from(fill.fill_type)
|
||||||
|
.map(|value| value.as_str_name().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("UNKNOWN({})", fill.fill_type))
|
||||||
|
});
|
||||||
|
return Ok(PcbItem::BoardGraphicShape(PcbBoardGraphicShape {
|
||||||
|
id: shape.id.map(|id| id.value),
|
||||||
|
layer: layer_to_model(shape.layer),
|
||||||
|
locked: map_lock_state(shape.locked),
|
||||||
|
net: map_optional_net(shape.net),
|
||||||
|
geometry_kind,
|
||||||
|
geometry,
|
||||||
|
stroke_width_nm,
|
||||||
|
stroke_style,
|
||||||
|
fill_type,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
|
||||||
|
let text = decode_any::<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 {
|
||||||
|
id: text.id.map(|id| id.value),
|
||||||
|
layer: layer_to_model(text.layer),
|
||||||
|
text: body,
|
||||||
|
position_nm,
|
||||||
|
hyperlink,
|
||||||
|
attributes,
|
||||||
|
knockout: text.knockout,
|
||||||
|
locked: map_lock_state(text.locked),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
|
||||||
|
let textbox =
|
||||||
|
decode_any::<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 {
|
||||||
|
id: textbox.id.map(|id| id.value),
|
||||||
|
layer: layer_to_model(textbox.layer),
|
||||||
|
text: body,
|
||||||
|
top_left_nm,
|
||||||
|
bottom_right_nm,
|
||||||
|
attributes,
|
||||||
|
locked: map_lock_state(textbox.locked),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Field") {
|
||||||
|
let field = decode_any::<board_types::Field>(&item, "kiapi.board.types.Field")?;
|
||||||
|
let text = field
|
||||||
|
.text
|
||||||
|
.and_then(|board_text| board_text.text)
|
||||||
|
.map(|value| value.text);
|
||||||
|
return Ok(PcbItem::Field(PcbField {
|
||||||
|
name: field.name,
|
||||||
|
visible: field.visible,
|
||||||
|
text,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("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 {
|
||||||
|
id: zone.id.map(|id| id.value),
|
||||||
|
name: zone.name,
|
||||||
|
zone_type: map_zone_type(zone.r#type),
|
||||||
|
layers,
|
||||||
|
layer_count: zone.layers.len(),
|
||||||
|
priority: zone.priority,
|
||||||
|
locked: map_lock_state(zone.locked),
|
||||||
|
filled: zone.filled,
|
||||||
|
polygon_count: zone.filled_polygons.len(),
|
||||||
|
outline_polygon_count: zone.outline.map_or(0, |outline| outline.polygons.len()),
|
||||||
|
has_copper_settings,
|
||||||
|
has_rule_area_settings,
|
||||||
|
border_style,
|
||||||
|
border_pitch_nm,
|
||||||
|
layer_properties,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
|
||||||
|
let dimension = decode_any::<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 {
|
||||||
|
id: dimension.id.map(|id| id.value),
|
||||||
|
layer: layer_to_model(dimension.layer),
|
||||||
|
locked: map_lock_state(dimension.locked),
|
||||||
|
text: dimension.text.map(|value| value.text),
|
||||||
|
style_kind,
|
||||||
|
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,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Group") {
|
||||||
|
let group = decode_any::<board_types::Group>(&item, "kiapi.board.types.Group")?;
|
||||||
|
return Ok(PcbItem::Group(PcbGroup {
|
||||||
|
id: group.id.map(|id| id.value),
|
||||||
|
name: group.name,
|
||||||
|
item_count: group.items.len(),
|
||||||
|
item_ids: group.items.into_iter().map(|item| item.value).collect(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(PcbItem::Unknown(PcbUnknownItem {
|
||||||
|
type_url: item.type_url,
|
||||||
|
raw_len: item.value.len(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn pad_netlist_from_footprint_items(
|
||||||
|
footprint_items: Vec<prost_types::Any>,
|
||||||
|
) -> Result<Vec<PadNetEntry>, KiCadError> {
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
for item in footprint_items {
|
||||||
|
if item.type_url != envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let footprint = decode_any::<board_types::FootprintInstance>(
|
||||||
|
&item,
|
||||||
|
"kiapi.board.types.FootprintInstance",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let footprint_reference = footprint
|
||||||
|
.reference_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 footprint_id = footprint.id.as_ref().map(|id| id.value.clone());
|
||||||
|
|
||||||
|
let footprint_definition = footprint.definition.unwrap_or_default();
|
||||||
|
for sub_item in footprint_definition.items {
|
||||||
|
if sub_item.type_url != envelope::type_url("kiapi.board.types.Pad") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pad = decode_any::<board_types::Pad>(&sub_item, "kiapi.board.types.Pad")?;
|
||||||
|
let (net_code, net_name) = match pad.net {
|
||||||
|
Some(net) => {
|
||||||
|
let code = net.code.map(|code| code.value);
|
||||||
|
let name = if net.name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(net.name)
|
||||||
|
};
|
||||||
|
(code, name)
|
||||||
|
}
|
||||||
|
None => (None, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
entries.push(PadNetEntry {
|
||||||
|
footprint_reference: footprint_reference.clone(),
|
||||||
|
footprint_id: footprint_id.clone(),
|
||||||
|
pad_id: pad.id.map(|id| id.value),
|
||||||
|
pad_number: pad.number,
|
||||||
|
net_code,
|
||||||
|
net_name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,197 @@
|
||||||
|
//! Document operations: save, revert, title block, and string serialization.
|
||||||
|
|
||||||
|
use super::decode::decode_pcb_items;
|
||||||
|
use super::mappers::*;
|
||||||
|
use super::{
|
||||||
|
KiCadClient, CMD_GET_ITEMS_BY_ID, CMD_GET_TITLE_BLOCK_INFO, CMD_REVERT_DOCUMENT,
|
||||||
|
CMD_SAVE_COPY_OF_DOCUMENT, CMD_SAVE_DOCUMENT, CMD_SAVE_DOCUMENT_TO_STRING,
|
||||||
|
CMD_SAVE_SELECTION_TO_STRING, RES_GET_ITEMS_RESPONSE, RES_PROTOBUF_EMPTY,
|
||||||
|
RES_SAVED_DOCUMENT_RESPONSE, RES_SAVED_SELECTION_RESPONSE, RES_TITLE_BLOCK_INFO,
|
||||||
|
};
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::model::common::*;
|
||||||
|
use crate::proto::kiapi::common::commands as common_commands;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Reads title block metadata from the active PCB document.
|
||||||
|
pub async fn get_title_block_info(&self) -> Result<TitleBlockInfo, KiCadError> {
|
||||||
|
let command = common_commands::GetTitleBlockInfo {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_TITLE_BLOCK_INFO))
|
||||||
|
.await?;
|
||||||
|
let payload: common_types::TitleBlockInfo =
|
||||||
|
envelope::unpack_any(&response, RES_TITLE_BLOCK_INFO)?;
|
||||||
|
|
||||||
|
let comments = vec![
|
||||||
|
payload.comment1,
|
||||||
|
payload.comment2,
|
||||||
|
payload.comment3,
|
||||||
|
payload.comment4,
|
||||||
|
payload.comment5,
|
||||||
|
payload.comment6,
|
||||||
|
payload.comment7,
|
||||||
|
payload.comment8,
|
||||||
|
payload.comment9,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.filter(|comment| !comment.is_empty())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(TitleBlockInfo {
|
||||||
|
title: payload.title,
|
||||||
|
date: payload.date,
|
||||||
|
revision: payload.revision,
|
||||||
|
company: payload.company,
|
||||||
|
comments,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the active PCB document and returns the raw operation payload.
|
||||||
|
pub async fn save_document_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::SaveDocument {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SAVE_DOCUMENT))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_PROTOBUF_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves the active PCB document.
|
||||||
|
pub async fn save_document(&self) -> Result<(), KiCadError> {
|
||||||
|
let _ = self.save_document_raw().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves a copy of the active PCB document and returns raw operation payload.
|
||||||
|
pub async fn save_copy_of_document_raw(
|
||||||
|
&self,
|
||||||
|
path: impl Into<String>,
|
||||||
|
overwrite: bool,
|
||||||
|
include_project: bool,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::SaveCopyOfDocument {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
path: path.into(),
|
||||||
|
options: Some(common_commands::SaveOptions {
|
||||||
|
overwrite,
|
||||||
|
include_project,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SAVE_COPY_OF_DOCUMENT))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_PROTOBUF_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Saves a copy of the active PCB document.
|
||||||
|
pub async fn save_copy_of_document(
|
||||||
|
&self,
|
||||||
|
path: impl Into<String>,
|
||||||
|
overwrite: bool,
|
||||||
|
include_project: bool,
|
||||||
|
) -> Result<(), KiCadError> {
|
||||||
|
let _ = self
|
||||||
|
.save_copy_of_document_raw(path, overwrite, include_project)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverts unsaved changes in the active PCB document and returns raw payload.
|
||||||
|
pub async fn revert_document_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::RevertDocument {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_REVERT_DOCUMENT))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_PROTOBUF_EMPTY)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reverts unsaved changes in the active PCB document.
|
||||||
|
pub async fn revert_document(&self) -> Result<(), KiCadError> {
|
||||||
|
let _ = self.revert_document_raw().await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes the active PCB document to KiCad's string format.
|
||||||
|
pub async fn get_board_as_string(&self) -> Result<String, KiCadError> {
|
||||||
|
let command = common_commands::SaveDocumentToString {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SAVE_DOCUMENT_TO_STRING))
|
||||||
|
.await?;
|
||||||
|
let payload: common_commands::SavedDocumentResponse =
|
||||||
|
envelope::unpack_any(&response, RES_SAVED_DOCUMENT_RESPONSE)?;
|
||||||
|
Ok(payload.contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serializes current selection to KiCad's string format.
|
||||||
|
pub async fn get_selection_as_string(&self) -> Result<SelectionStringDump, KiCadError> {
|
||||||
|
let command = common_commands::SaveSelectionToString {};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_SAVE_SELECTION_TO_STRING))
|
||||||
|
.await?;
|
||||||
|
let payload: common_commands::SavedSelectionResponse =
|
||||||
|
envelope::unpack_any(&response, RES_SAVED_SELECTION_RESPONSE)?;
|
||||||
|
Ok(SelectionStringDump {
|
||||||
|
ids: payload.ids.into_iter().map(|id| id.value).collect(),
|
||||||
|
contents: payload.contents,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items by id and returns raw protobuf payloads.
|
||||||
|
pub async fn get_items_by_id_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = common_commands::GetItemsById {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| common_types::Kiid { value: id })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_ID))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetItemsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
|
||||||
|
ensure_item_request_ok(payload.status)?;
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items by id and returns lightweight decoded detail rows.
|
||||||
|
pub async fn get_items_by_id_details(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
|
let items = self.get_items_by_id_raw(item_ids).await?;
|
||||||
|
summarize_item_details(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches and decodes items by KiCad item id.
|
||||||
|
pub async fn get_items_by_id(&self, item_ids: Vec<String>) -> Result<Vec<PcbItem>, KiCadError> {
|
||||||
|
let items = self.get_items_by_id_raw(item_ids).await?;
|
||||||
|
decode_pcb_items(items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,349 @@
|
||||||
|
//! Human-readable formatting for PCB items and debug utilities.
|
||||||
|
|
||||||
|
use super::mappers::*;
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::proto::kiapi::board::types as board_types;
|
||||||
|
pub(crate) fn selection_item_detail(item: &prost_types::Any) -> Result<String, KiCadError> {
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Track") {
|
||||||
|
let track = decode_any::<board_types::Track>(item, "kiapi.board.types.Track")?;
|
||||||
|
return Ok(format_track_selection_detail(track));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Arc") {
|
||||||
|
let arc = decode_any::<board_types::Arc>(item, "kiapi.board.types.Arc")?;
|
||||||
|
return Ok(format_arc_selection_detail(arc));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Via") {
|
||||||
|
let via = decode_any::<board_types::Via>(item, "kiapi.board.types.Via")?;
|
||||||
|
return Ok(format_via_selection_detail(via));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.FootprintInstance") {
|
||||||
|
let footprint = decode_any::<board_types::FootprintInstance>(
|
||||||
|
item,
|
||||||
|
"kiapi.board.types.FootprintInstance",
|
||||||
|
)?;
|
||||||
|
return Ok(format_footprint_selection_detail(footprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Field") {
|
||||||
|
let field = decode_any::<board_types::Field>(item, "kiapi.board.types.Field")?;
|
||||||
|
return Ok(format_field_selection_detail(field));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardText") {
|
||||||
|
let text = decode_any::<board_types::BoardText>(item, "kiapi.board.types.BoardText")?;
|
||||||
|
return Ok(format_board_text_selection_detail(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardTextBox") {
|
||||||
|
let textbox =
|
||||||
|
decode_any::<board_types::BoardTextBox>(item, "kiapi.board.types.BoardTextBox")?;
|
||||||
|
return Ok(format_board_textbox_selection_detail(textbox));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Pad") {
|
||||||
|
let pad = decode_any::<board_types::Pad>(item, "kiapi.board.types.Pad")?;
|
||||||
|
return Ok(format_pad_selection_detail(pad));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.BoardGraphicShape") {
|
||||||
|
let shape = decode_any::<board_types::BoardGraphicShape>(
|
||||||
|
item,
|
||||||
|
"kiapi.board.types.BoardGraphicShape",
|
||||||
|
)?;
|
||||||
|
return Ok(format_board_graphic_shape_selection_detail(shape));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Zone") {
|
||||||
|
let zone = decode_any::<board_types::Zone>(item, "kiapi.board.types.Zone")?;
|
||||||
|
return Ok(format_zone_selection_detail(zone));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Dimension") {
|
||||||
|
let dimension = decode_any::<board_types::Dimension>(item, "kiapi.board.types.Dimension")?;
|
||||||
|
return Ok(format_dimension_selection_detail(dimension));
|
||||||
|
}
|
||||||
|
|
||||||
|
if item.type_url == envelope::type_url("kiapi.board.types.Group") {
|
||||||
|
let group = decode_any::<board_types::Group>(item, "kiapi.board.types.Group")?;
|
||||||
|
return Ok(format_group_selection_detail(group));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(format!("unparsed payload ({} bytes)", item.value.len()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_track_selection_detail(track: board_types::Track) -> String {
|
||||||
|
let id = track.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let start = track
|
||||||
|
.start
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let end = track
|
||||||
|
.end
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let width = track
|
||||||
|
.width
|
||||||
|
.map_or_else(|| "-".to_string(), |w| w.value_nm.to_string());
|
||||||
|
let layer = layer_to_model(track.layer).name;
|
||||||
|
let net = track
|
||||||
|
.net
|
||||||
|
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!("track id={id} start_nm={start} end_nm={end} width_nm={width} layer={layer} net={net}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_arc_selection_detail(arc: board_types::Arc) -> String {
|
||||||
|
let id = arc.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let start = arc
|
||||||
|
.start
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let mid = arc
|
||||||
|
.mid
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let end = arc
|
||||||
|
.end
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let width = arc
|
||||||
|
.width
|
||||||
|
.map_or_else(|| "-".to_string(), |w| w.value_nm.to_string());
|
||||||
|
let layer = layer_to_model(arc.layer).name;
|
||||||
|
let net = arc
|
||||||
|
.net
|
||||||
|
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!(
|
||||||
|
"arc id={id} start_nm={start} mid_nm={mid} end_nm={end} width_nm={width} layer={layer} net={net}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_via_selection_detail(via: board_types::Via) -> String {
|
||||||
|
let id = via.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let position = via
|
||||||
|
.position
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let net = via
|
||||||
|
.net
|
||||||
|
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
let via_type = board_types::ViaType::try_from(via.r#type)
|
||||||
|
.map(|value| value.as_str_name().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("UNKNOWN({})", via.r#type));
|
||||||
|
let layers = map_via_layers(via.pad_stack.as_ref());
|
||||||
|
let pad_layers = layers
|
||||||
|
.as_ref()
|
||||||
|
.map(|row| format_layer_names(&row.padstack_layers))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
let drill_start = layers
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|row| row.drill_start_layer.as_ref())
|
||||||
|
.map(|layer| layer.name.as_str())
|
||||||
|
.unwrap_or("-");
|
||||||
|
let drill_end = layers
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|row| row.drill_end_layer.as_ref())
|
||||||
|
.map(|layer| layer.name.as_str())
|
||||||
|
.unwrap_or("-");
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"via id={id} pos_nm={position} type={via_type} net={net} pad_layers={pad_layers} drill_span={drill_start}->{drill_end}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_layer_names(layers: &[BoardLayerInfo]) -> String {
|
||||||
|
if layers.is_empty() {
|
||||||
|
return "-".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
layers
|
||||||
|
.iter()
|
||||||
|
.map(|layer| layer.name.as_str())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_footprint_selection_detail(
|
||||||
|
footprint: board_types::FootprintInstance,
|
||||||
|
) -> String {
|
||||||
|
let id = footprint.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let reference = footprint
|
||||||
|
.reference_field
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|field| field.text.as_ref())
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
let position = footprint
|
||||||
|
.position
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let orientation_deg = footprint.orientation.map_or_else(
|
||||||
|
|| "-".to_string(),
|
||||||
|
|orientation| orientation.value_degrees.to_string(),
|
||||||
|
);
|
||||||
|
let layer = layer_to_model(footprint.layer).name;
|
||||||
|
let pad_count = footprint
|
||||||
|
.definition
|
||||||
|
.as_ref()
|
||||||
|
.map(|definition| {
|
||||||
|
definition
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.filter(|entry| entry.type_url == envelope::type_url("kiapi.board.types.Pad"))
|
||||||
|
.count()
|
||||||
|
})
|
||||||
|
.unwrap_or(0);
|
||||||
|
format!(
|
||||||
|
"footprint id={id} ref={reference} pos_nm={position} orientation_deg={orientation_deg} layer={layer} pad_count={pad_count}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_field_selection_detail(field: board_types::Field) -> String {
|
||||||
|
let text = field
|
||||||
|
.text
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|board_text| board_text.text.as_ref())
|
||||||
|
.map(|text| text.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!(
|
||||||
|
"field name={} visible={} text={}",
|
||||||
|
field.name, field.visible, text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_board_text_selection_detail(text: board_types::BoardText) -> String {
|
||||||
|
let id = text.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let layer = layer_to_model(text.layer).name;
|
||||||
|
let body = text
|
||||||
|
.text
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!("text id={id} layer={layer} text={body}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_board_textbox_selection_detail(textbox: board_types::BoardTextBox) -> String {
|
||||||
|
let id = textbox.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let layer = layer_to_model(textbox.layer).name;
|
||||||
|
let body = textbox
|
||||||
|
.textbox
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!("textbox id={id} layer={layer} text={body}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_pad_selection_detail(pad: board_types::Pad) -> String {
|
||||||
|
let id = pad.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let pad_type = board_types::PadType::try_from(pad.r#type)
|
||||||
|
.map(|value| value.as_str_name().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("UNKNOWN({})", pad.r#type));
|
||||||
|
let position = pad
|
||||||
|
.position
|
||||||
|
.map_or_else(|| "-".to_string(), |v| format!("{},{}", v.x_nm, v.y_nm));
|
||||||
|
let net = pad
|
||||||
|
.net
|
||||||
|
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!(
|
||||||
|
"pad id={id} number={} type={pad_type} pos_nm={position} net={net}",
|
||||||
|
pad.number
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_board_graphic_shape_selection_detail(
|
||||||
|
shape: board_types::BoardGraphicShape,
|
||||||
|
) -> String {
|
||||||
|
let id = shape.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let layer = layer_to_model(shape.layer).name;
|
||||||
|
let net = shape
|
||||||
|
.net
|
||||||
|
.map(|n| format!("{}:{}", n.code.map_or(0, |c| c.value), n.name))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
let geometry = shape
|
||||||
|
.shape
|
||||||
|
.as_ref()
|
||||||
|
.map(|graphic| format!("{:?}", graphic.geometry))
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
format!("graphic id={id} layer={layer} net={net} geometry={geometry}")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_zone_selection_detail(zone: board_types::Zone) -> String {
|
||||||
|
let id = zone.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let zone_type = board_types::ZoneType::try_from(zone.r#type)
|
||||||
|
.map(|value| value.as_str_name().to_string())
|
||||||
|
.unwrap_or_else(|_| format!("UNKNOWN({})", zone.r#type));
|
||||||
|
format!(
|
||||||
|
"zone id={id} name={} type={} layer_count={} filled={} polygon_count={}",
|
||||||
|
zone.name,
|
||||||
|
zone_type,
|
||||||
|
zone.layers.len(),
|
||||||
|
zone.filled,
|
||||||
|
zone.filled_polygons.len()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_dimension_selection_detail(dimension: board_types::Dimension) -> String {
|
||||||
|
let id = dimension.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
let layer = layer_to_model(dimension.layer).name;
|
||||||
|
let text = dimension
|
||||||
|
.text
|
||||||
|
.as_ref()
|
||||||
|
.map(|value| value.text.clone())
|
||||||
|
.unwrap_or_else(|| "-".to_string());
|
||||||
|
let style = format!("{:?}", dimension.dimension_style);
|
||||||
|
format!(
|
||||||
|
"dimension id={id} layer={layer} text={} style={style}",
|
||||||
|
text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn format_group_selection_detail(group: board_types::Group) -> String {
|
||||||
|
let id = group.id.map_or_else(|| "-".to_string(), |id| id.value);
|
||||||
|
format!(
|
||||||
|
"group id={id} name={} item_count={}",
|
||||||
|
group.name,
|
||||||
|
group.items.len()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn any_to_pretty_debug(item: &prost_types::Any) -> Result<String, KiCadError> {
|
||||||
|
macro_rules! debug_any {
|
||||||
|
($(($url:literal, $ty:ty)),* $(,)?) => {
|
||||||
|
$(
|
||||||
|
if item.type_url == envelope::type_url($url) {
|
||||||
|
let value = decode_any::<$ty>(item, $url)?;
|
||||||
|
return Ok(format!("{:#?}", value));
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
debug_any!(
|
||||||
|
("kiapi.board.types.Track", board_types::Track),
|
||||||
|
("kiapi.board.types.Arc", board_types::Arc),
|
||||||
|
("kiapi.board.types.Via", board_types::Via),
|
||||||
|
(
|
||||||
|
"kiapi.board.types.FootprintInstance",
|
||||||
|
board_types::FootprintInstance
|
||||||
|
),
|
||||||
|
("kiapi.board.types.Pad", board_types::Pad),
|
||||||
|
(
|
||||||
|
"kiapi.board.types.BoardGraphicShape",
|
||||||
|
board_types::BoardGraphicShape
|
||||||
|
),
|
||||||
|
("kiapi.board.types.BoardText", board_types::BoardText),
|
||||||
|
("kiapi.board.types.BoardTextBox", board_types::BoardTextBox),
|
||||||
|
("kiapi.board.types.Field", board_types::Field),
|
||||||
|
("kiapi.board.types.Zone", board_types::Zone),
|
||||||
|
("kiapi.board.types.Dimension", board_types::Dimension),
|
||||||
|
("kiapi.board.types.Group", board_types::Group),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"unparsed_any type_url={} raw_len={}",
|
||||||
|
item.type_url,
|
||||||
|
item.value.len()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
//! Geometry queries: bounding boxes, hit testing, pad polygons, padstack presence, and zone refill.
|
||||||
|
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::model::common::*;
|
||||||
|
use crate::proto::kiapi::board::commands as board_commands;
|
||||||
|
use crate::proto::kiapi::common::commands as common_commands;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
|
||||||
|
use super::mappers::*;
|
||||||
|
use super::{
|
||||||
|
KiCadClient, CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS, CMD_GET_BOUNDING_BOX,
|
||||||
|
CMD_GET_PAD_SHAPE_AS_POLYGON, CMD_HIT_TEST, CMD_REFILL_ZONES, PAD_QUERY_CHUNK_SIZE,
|
||||||
|
RES_GET_BOUNDING_BOX_RESPONSE, RES_HIT_TEST_RESPONSE, RES_PADSTACK_PRESENCE_RESPONSE,
|
||||||
|
RES_PAD_SHAPE_AS_POLYGON_RESPONSE, RES_PROTOBUF_EMPTY,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Rebuilds fill geometry for the given zone ids.
|
||||||
|
pub async fn refill_zones(&self, zone_ids: Vec<String>) -> Result<(), KiCadError> {
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let command = board_commands::RefillZones {
|
||||||
|
board: Some(board),
|
||||||
|
zones: zone_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_REFILL_ZONES))
|
||||||
|
.await?;
|
||||||
|
let _ = response_payload_as_any(response, RES_PROTOBUF_EMPTY)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
/// Returns pad polygon responses as raw protobuf payloads.
|
||||||
|
pub async fn get_pad_shape_as_polygon_raw(
|
||||||
|
&self,
|
||||||
|
pad_ids: Vec<String>,
|
||||||
|
layer_id: i32,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
if pad_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let mut payloads = Vec::new();
|
||||||
|
for chunk in pad_ids.chunks(PAD_QUERY_CHUNK_SIZE) {
|
||||||
|
let command = board_commands::GetPadShapeAsPolygon {
|
||||||
|
board: Some(board.clone()),
|
||||||
|
pads: chunk
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
layer: layer_id,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_PAD_SHAPE_AS_POLYGON))
|
||||||
|
.await?;
|
||||||
|
payloads.push(response_payload_as_any(
|
||||||
|
response,
|
||||||
|
RES_PAD_SHAPE_AS_POLYGON_RESPONSE,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(payloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mapped pad polygons for the requested layer.
|
||||||
|
pub async fn get_pad_shape_as_polygon(
|
||||||
|
&self,
|
||||||
|
pad_ids: Vec<String>,
|
||||||
|
layer_id: i32,
|
||||||
|
) -> Result<Vec<PadShapeAsPolygonEntry>, KiCadError> {
|
||||||
|
if pad_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let layer_name = layer_to_model(layer_id).name;
|
||||||
|
|
||||||
|
let payloads = self.get_pad_shape_as_polygon_raw(pad_ids, layer_id).await?;
|
||||||
|
for payload in payloads {
|
||||||
|
let payload: board_commands::PadShapeAsPolygonResponse =
|
||||||
|
decode_any(&payload, RES_PAD_SHAPE_AS_POLYGON_RESPONSE)?;
|
||||||
|
|
||||||
|
if payload.pads.len() != payload.polygons.len() {
|
||||||
|
return Err(KiCadError::InvalidResponse {
|
||||||
|
reason: format!(
|
||||||
|
"GetPadShapeAsPolygon returned mismatched arrays: pads={}, polygons={}",
|
||||||
|
payload.pads.len(),
|
||||||
|
payload.polygons.len()
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (pad, polygon) in payload.pads.into_iter().zip(payload.polygons.into_iter()) {
|
||||||
|
entries.push(PadShapeAsPolygonEntry {
|
||||||
|
pad_id: pad.value,
|
||||||
|
layer_id,
|
||||||
|
layer_name: layer_name.clone(),
|
||||||
|
polygon: map_polygon_with_holes(polygon)?,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns padstack presence responses as raw protobuf payloads.
|
||||||
|
pub async fn check_padstack_presence_on_layers_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
layer_ids: Vec<i32>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
if item_ids.is_empty() || layer_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let board = self.current_board_document_proto().await?;
|
||||||
|
let mut payloads = Vec::new();
|
||||||
|
for chunk in item_ids.chunks(PAD_QUERY_CHUNK_SIZE) {
|
||||||
|
let command = board_commands::CheckPadstackPresenceOnLayers {
|
||||||
|
board: Some(board.clone()),
|
||||||
|
items: chunk
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
layers: layer_ids.clone(),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(
|
||||||
|
&command,
|
||||||
|
CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
payloads.push(response_payload_as_any(
|
||||||
|
response,
|
||||||
|
RES_PADSTACK_PRESENCE_RESPONSE,
|
||||||
|
)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(payloads)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns mapped padstack presence for item and layer combinations.
|
||||||
|
pub async fn check_padstack_presence_on_layers(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
layer_ids: Vec<i32>,
|
||||||
|
) -> Result<Vec<PadstackPresenceEntry>, KiCadError> {
|
||||||
|
if item_ids.is_empty() || layer_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let payloads = self
|
||||||
|
.check_padstack_presence_on_layers_raw(item_ids, layer_ids)
|
||||||
|
.await?;
|
||||||
|
for payload in payloads {
|
||||||
|
let payload: board_commands::PadstackPresenceResponse =
|
||||||
|
decode_any(&payload, RES_PADSTACK_PRESENCE_RESPONSE)?;
|
||||||
|
for row in payload.entries {
|
||||||
|
let item = row.item.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "PadstackPresenceEntry missing item id".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let layer = layer_to_model(row.layer);
|
||||||
|
let presence = map_padstack_presence(row.presence);
|
||||||
|
|
||||||
|
entries.push(PadstackPresenceEntry {
|
||||||
|
item_id: item.value,
|
||||||
|
layer_id: row.layer,
|
||||||
|
layer_name: layer.name,
|
||||||
|
presence,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns axis-aligned bounding boxes for item ids.
|
||||||
|
pub async fn get_item_bounding_boxes(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
include_child_text: bool,
|
||||||
|
) -> Result<Vec<ItemBoundingBox>, KiCadError> {
|
||||||
|
if item_ids.is_empty() {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mode = if include_child_text {
|
||||||
|
common_commands::BoundingBoxMode::BbmItemAndChildText
|
||||||
|
} else {
|
||||||
|
common_commands::BoundingBoxMode::BbmItemOnly
|
||||||
|
};
|
||||||
|
|
||||||
|
let command = common_commands::GetBoundingBox {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|id| common_types::Kiid { value: id })
|
||||||
|
.collect(),
|
||||||
|
mode: mode as i32,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_BOUNDING_BOX))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetBoundingBoxResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_BOUNDING_BOX_RESPONSE)?;
|
||||||
|
|
||||||
|
map_item_bounding_boxes(payload.items, payload.boxes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs hit-test for a specific item at a position with tolerance.
|
||||||
|
pub async fn hit_test_item(
|
||||||
|
&self,
|
||||||
|
item_id: String,
|
||||||
|
position: Vector2Nm,
|
||||||
|
tolerance_nm: i32,
|
||||||
|
) -> Result<ItemHitTestResult, KiCadError> {
|
||||||
|
let command = common_commands::HitTest {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
id: Some(common_types::Kiid { value: item_id }),
|
||||||
|
position: Some(common_types::Vector2 {
|
||||||
|
x_nm: position.x_nm,
|
||||||
|
y_nm: position.y_nm,
|
||||||
|
}),
|
||||||
|
tolerance: tolerance_nm,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_HIT_TEST))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::HitTestResponse =
|
||||||
|
envelope::unpack_any(&response, RES_HIT_TEST_RESPONSE)?;
|
||||||
|
|
||||||
|
Ok(map_hit_test_result(payload.result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,454 @@
|
||||||
|
//! Item CRUD operations: create, update, delete, query, and commit workflows.
|
||||||
|
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::model::common::*;
|
||||||
|
use crate::proto::kiapi::board::commands as board_commands;
|
||||||
|
use crate::proto::kiapi::board::types as board_types;
|
||||||
|
use crate::proto::kiapi::common::commands as common_commands;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
|
||||||
|
use super::decode::*;
|
||||||
|
use super::format::*;
|
||||||
|
use super::mappers::*;
|
||||||
|
use super::{
|
||||||
|
KiCadClient, CMD_BEGIN_COMMIT, CMD_CREATE_ITEMS, CMD_DELETE_ITEMS, CMD_END_COMMIT,
|
||||||
|
CMD_GET_ITEMS_BY_NET, CMD_GET_ITEMS_BY_NET_CLASS, CMD_GET_NETCLASS_FOR_NETS,
|
||||||
|
CMD_PARSE_AND_CREATE_ITEMS_FROM_STRING, CMD_UPDATE_ITEMS, PCB_OBJECT_TYPES,
|
||||||
|
RES_BEGIN_COMMIT_RESPONSE, RES_CREATE_ITEMS_RESPONSE, RES_DELETE_ITEMS_RESPONSE,
|
||||||
|
RES_END_COMMIT_RESPONSE, RES_GET_ITEMS_RESPONSE, RES_NETCLASS_FOR_NETS_RESPONSE,
|
||||||
|
RES_UPDATE_ITEMS_RESPONSE,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Starts a commit session and returns the raw begin-commit payload.
|
||||||
|
pub async fn begin_commit_raw(&self) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::BeginCommit {};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_BEGIN_COMMIT))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_BEGIN_COMMIT_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a KiCad commit session used for grouped board edits.
|
||||||
|
pub async fn begin_commit(&self) -> Result<CommitSession, KiCadError> {
|
||||||
|
let payload = self.begin_commit_raw().await?;
|
||||||
|
let response: common_commands::BeginCommitResponse =
|
||||||
|
decode_any(&payload, RES_BEGIN_COMMIT_RESPONSE)?;
|
||||||
|
map_commit_session(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ends a commit session and returns the raw end-commit payload.
|
||||||
|
pub async fn end_commit_raw(
|
||||||
|
&self,
|
||||||
|
session: CommitSession,
|
||||||
|
action: CommitAction,
|
||||||
|
message: impl Into<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
if session.id.is_empty() {
|
||||||
|
return Err(KiCadError::Config {
|
||||||
|
reason: "end_commit_raw requires a non-empty commit session id".to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let command = common_commands::EndCommit {
|
||||||
|
id: Some(common_types::Kiid { value: session.id }),
|
||||||
|
action: commit_action_to_proto(action),
|
||||||
|
message: message.into(),
|
||||||
|
};
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_END_COMMIT))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_END_COMMIT_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finalizes a commit session, either committing or dropping staged changes.
|
||||||
|
pub async fn end_commit(
|
||||||
|
&self,
|
||||||
|
session: CommitSession,
|
||||||
|
action: CommitAction,
|
||||||
|
message: impl Into<String>,
|
||||||
|
) -> Result<(), KiCadError> {
|
||||||
|
self.end_commit_raw(session, action, message).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates items and returns the raw create-items payload.
|
||||||
|
pub async fn create_items_raw(
|
||||||
|
&self,
|
||||||
|
items: Vec<prost_types::Any>,
|
||||||
|
container_id: Option<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::CreateItems {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items,
|
||||||
|
container: container_id.map(|value| common_types::Kiid { value }),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_CREATE_ITEMS))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_CREATE_ITEMS_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates items in the active PCB document.
|
||||||
|
///
|
||||||
|
/// Returns created items as raw protobuf `Any` payloads.
|
||||||
|
pub async fn create_items(
|
||||||
|
&self,
|
||||||
|
items: Vec<prost_types::Any>,
|
||||||
|
container_id: Option<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let payload = self.create_items_raw(items, container_id).await?;
|
||||||
|
let response: common_commands::CreateItemsResponse =
|
||||||
|
decode_any(&payload, RES_CREATE_ITEMS_RESPONSE)?;
|
||||||
|
ensure_item_request_ok(response.status)?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.created_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
ensure_item_status_ok(row.status)?;
|
||||||
|
row.item.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "CreateItemsResponse missing created item payload".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates items and returns the raw update-items payload.
|
||||||
|
pub async fn update_items_raw(
|
||||||
|
&self,
|
||||||
|
items: Vec<prost_types::Any>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::UpdateItems {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_UPDATE_ITEMS))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_UPDATE_ITEMS_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates existing items in the active PCB document.
|
||||||
|
///
|
||||||
|
/// Returns updated items as raw protobuf `Any` payloads.
|
||||||
|
pub async fn update_items(
|
||||||
|
&self,
|
||||||
|
items: Vec<prost_types::Any>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let payload = self.update_items_raw(items).await?;
|
||||||
|
let response: common_commands::UpdateItemsResponse =
|
||||||
|
decode_any(&payload, RES_UPDATE_ITEMS_RESPONSE)?;
|
||||||
|
ensure_item_request_ok(response.status)?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.updated_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
ensure_item_status_ok(row.status)?;
|
||||||
|
row.item.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "UpdateItemsResponse missing updated item payload".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes items and returns the raw delete-items payload.
|
||||||
|
pub async fn delete_items_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::DeleteItems {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
item_ids: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_DELETE_ITEMS))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_DELETE_ITEMS_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes items by id from the active PCB document.
|
||||||
|
///
|
||||||
|
/// Returns ids of items deleted by KiCad.
|
||||||
|
pub async fn delete_items(&self, item_ids: Vec<String>) -> Result<Vec<String>, KiCadError> {
|
||||||
|
let payload = self.delete_items_raw(item_ids).await?;
|
||||||
|
let response: common_commands::DeleteItemsResponse =
|
||||||
|
decode_any(&payload, RES_DELETE_ITEMS_RESPONSE)?;
|
||||||
|
ensure_item_request_ok(response.status)?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.deleted_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
ensure_item_deletion_status_ok(row.status)?;
|
||||||
|
row.id
|
||||||
|
.map(|id| id.value)
|
||||||
|
.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "DeleteItemsResponse missing deleted item id".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses KiCad item text and creates items, returning raw create-items payload.
|
||||||
|
pub async fn parse_and_create_items_from_string_raw(
|
||||||
|
&self,
|
||||||
|
contents: impl Into<String>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = common_commands::ParseAndCreateItemsFromString {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
contents: contents.into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(
|
||||||
|
&command,
|
||||||
|
CMD_PARSE_AND_CREATE_ITEMS_FROM_STRING,
|
||||||
|
))
|
||||||
|
.await?;
|
||||||
|
response_payload_as_any(response, RES_CREATE_ITEMS_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses KiCad item text and returns created items as raw payloads.
|
||||||
|
pub async fn parse_and_create_items_from_string(
|
||||||
|
&self,
|
||||||
|
contents: impl Into<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let payload = self
|
||||||
|
.parse_and_create_items_from_string_raw(contents)
|
||||||
|
.await?;
|
||||||
|
let response: common_commands::CreateItemsResponse =
|
||||||
|
decode_any(&payload, RES_CREATE_ITEMS_RESPONSE)?;
|
||||||
|
ensure_item_request_ok(response.status)?;
|
||||||
|
|
||||||
|
response
|
||||||
|
.created_items
|
||||||
|
.into_iter()
|
||||||
|
.map(|row| {
|
||||||
|
ensure_item_status_ok(row.status)?;
|
||||||
|
row.item.ok_or_else(|| KiCadError::InvalidResponse {
|
||||||
|
reason: "CreateItemsResponse missing created item payload".to_string(),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `(pad_id, net)` mappings derived from footprint items.
|
||||||
|
pub async fn get_pad_netlist(&self) -> Result<Vec<PadNetEntry>, KiCadError> {
|
||||||
|
let footprint_items = self
|
||||||
|
.get_items_raw(vec![common_types::KiCadObjectType::KotPcbFootprint as i32])
|
||||||
|
.await?;
|
||||||
|
pad_netlist_from_footprint_items(footprint_items)
|
||||||
|
}
|
||||||
|
/// Returns vias as raw protobuf payloads.
|
||||||
|
pub async fn get_vias_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
self.get_items_raw(vec![common_types::KiCadObjectType::KotPcbVia as i32])
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns vias decoded into typed [`PcbVia`] entries.
|
||||||
|
pub async fn get_vias(&self) -> Result<Vec<PcbVia>, KiCadError> {
|
||||||
|
let items = self
|
||||||
|
.get_items_by_type_codes(vec![common_types::KiCadObjectType::KotPcbVia as i32])
|
||||||
|
.await?;
|
||||||
|
Ok(items
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|item| match item {
|
||||||
|
PcbItem::Via(via) => Some(via),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns known KiCad PCB object type codes handled by this crate.
|
||||||
|
pub fn pcb_object_type_codes() -> &'static [PcbObjectTypeCode] {
|
||||||
|
&PCB_OBJECT_TYPES
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves a human-readable object type name from a KiCad object type code.
|
||||||
|
pub fn pcb_object_type_name(type_code: i32) -> Option<&'static str> {
|
||||||
|
PCB_OBJECT_TYPES
|
||||||
|
.iter()
|
||||||
|
.find(|entry| entry.code == type_code)
|
||||||
|
.map(|entry| entry.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats a raw protobuf PCB item payload for debugging/logging.
|
||||||
|
pub fn debug_any_item(item: &prost_types::Any) -> Result<String, KiCadError> {
|
||||||
|
any_to_pretty_debug(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items by object type codes and returns raw protobuf payloads.
|
||||||
|
pub async fn get_items_raw_by_type_codes(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
self.get_items_raw(type_codes).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches item details by object type codes.
|
||||||
|
pub async fn get_items_details_by_type_codes(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
|
let items = self.get_items_raw(type_codes).await?;
|
||||||
|
summarize_item_details(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches and decodes items by KiCad object type codes.
|
||||||
|
pub async fn get_items_by_type_codes(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<PcbItem>, KiCadError> {
|
||||||
|
let items = self.get_items_raw(type_codes).await?;
|
||||||
|
decode_pcb_items(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches all known object type buckets and returns raw payloads.
|
||||||
|
pub async fn get_all_pcb_items_raw(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<(PcbObjectTypeCode, Vec<prost_types::Any>)>, KiCadError> {
|
||||||
|
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
|
||||||
|
for object_type in PCB_OBJECT_TYPES {
|
||||||
|
let items = self.get_items_raw(vec![object_type.code]).await?;
|
||||||
|
rows.push((object_type, items));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches all known object type buckets and returns decoded detail rows.
|
||||||
|
pub async fn get_all_pcb_items_details(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<(PcbObjectTypeCode, Vec<SelectionItemDetail>)>, KiCadError> {
|
||||||
|
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
|
||||||
|
for object_type in PCB_OBJECT_TYPES {
|
||||||
|
let items = self.get_items_raw(vec![object_type.code]).await?;
|
||||||
|
rows.push((object_type, summarize_item_details(items)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches all known PCB item kinds and decodes each bucket.
|
||||||
|
pub async fn get_all_pcb_items(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<(PcbObjectTypeCode, Vec<PcbItem>)>, KiCadError> {
|
||||||
|
let mut rows = Vec::with_capacity(PCB_OBJECT_TYPES.len());
|
||||||
|
for object_type in PCB_OBJECT_TYPES {
|
||||||
|
let items = self.get_items_raw(vec![object_type.code]).await?;
|
||||||
|
rows.push((object_type, decode_pcb_items(items)?));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items filtered by net codes and returns raw protobuf payloads.
|
||||||
|
pub async fn get_items_by_net_raw(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
net_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = board_commands::GetItemsByNet {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
types: type_codes,
|
||||||
|
net_codes: net_codes
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| board_types::NetCode { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_NET))
|
||||||
|
.await?;
|
||||||
|
let payload: common_commands::GetItemsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
ensure_item_request_ok(payload.status)?;
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items filtered by net codes and decodes typed items.
|
||||||
|
pub async fn get_items_by_net(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
net_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<PcbItem>, KiCadError> {
|
||||||
|
let items = self.get_items_by_net_raw(type_codes, net_codes).await?;
|
||||||
|
decode_pcb_items(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items filtered by net class names and returns raw payloads.
|
||||||
|
pub async fn get_items_by_net_class_raw(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
net_classes: Vec<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = board_commands::GetItemsByNetClass {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
types: type_codes,
|
||||||
|
net_classes,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS_BY_NET_CLASS))
|
||||||
|
.await?;
|
||||||
|
let payload: common_commands::GetItemsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
ensure_item_request_ok(payload.status)?;
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches items filtered by net class names and decodes typed items.
|
||||||
|
pub async fn get_items_by_net_class(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
net_classes: Vec<String>,
|
||||||
|
) -> Result<Vec<PcbItem>, KiCadError> {
|
||||||
|
let items = self
|
||||||
|
.get_items_by_net_class_raw(type_codes, net_classes)
|
||||||
|
.await?;
|
||||||
|
decode_pcb_items(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves net class assignments for nets and returns raw response payload.
|
||||||
|
pub async fn get_netclass_for_nets_raw(
|
||||||
|
&self,
|
||||||
|
nets: Vec<BoardNet>,
|
||||||
|
) -> Result<prost_types::Any, KiCadError> {
|
||||||
|
let command = board_commands::GetNetClassForNets {
|
||||||
|
net: nets
|
||||||
|
.into_iter()
|
||||||
|
.map(|net| board_types::Net {
|
||||||
|
code: Some(board_types::NetCode { value: net.code }),
|
||||||
|
name: net.name,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_NETCLASS_FOR_NETS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
response_payload_as_any(response, RES_NETCLASS_FOR_NETS_RESPONSE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolves net class assignments for nets.
|
||||||
|
pub async fn get_netclass_for_nets(
|
||||||
|
&self,
|
||||||
|
nets: Vec<BoardNet>,
|
||||||
|
) -> Result<Vec<NetClassForNetEntry>, KiCadError> {
|
||||||
|
let payload = self.get_netclass_for_nets_raw(nets).await?;
|
||||||
|
let response: board_commands::NetClassForNetsResponse =
|
||||||
|
decode_any(&payload, RES_NETCLASS_FOR_NETS_RESPONSE)?;
|
||||||
|
Ok(map_netclass_for_nets_response(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,638 @@
|
||||||
|
mod board;
|
||||||
|
mod common;
|
||||||
|
mod decode;
|
||||||
|
mod document;
|
||||||
|
mod format;
|
||||||
|
mod geometry;
|
||||||
|
mod items;
|
||||||
|
mod mappers;
|
||||||
|
mod selection;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
use self::mappers::*;
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::common::*;
|
||||||
|
use crate::proto::kiapi::common::commands as common_commands;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
use crate::transport::Transport;
|
||||||
|
|
||||||
|
/// Sends a protobuf command and validates the response type URL.
|
||||||
|
///
|
||||||
|
/// This macro reduces boilerplate in the `_raw` RPC methods. It packs
|
||||||
|
/// the given command, sends it, and returns a validated `prost_types::Any`.
|
||||||
|
macro_rules! rpc {
|
||||||
|
($self:expr, $cmd_type_url:expr, $command:expr, $res_type_url:expr) => {{
|
||||||
|
let response = $self
|
||||||
|
.send_command(crate::envelope::pack_any(&$command, $cmd_type_url))
|
||||||
|
.await?;
|
||||||
|
super::mappers::response_payload_as_any(response, $res_type_url)
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) use rpc;
|
||||||
|
|
||||||
|
pub(crate) const KICAD_API_SOCKET_ENV: &str = "KICAD_API_SOCKET";
|
||||||
|
pub(crate) const KICAD_API_TOKEN_ENV: &str = "KICAD_API_TOKEN";
|
||||||
|
pub(crate) const KIPRJMOD_ENV: &str = "KIPRJMOD";
|
||||||
|
pub(crate) const CMD_PING: &str = "kiapi.common.commands.Ping";
|
||||||
|
pub(crate) const CMD_GET_VERSION: &str = "kiapi.common.commands.GetVersion";
|
||||||
|
pub(crate) const CMD_GET_KICAD_BINARY_PATH: &str = "kiapi.common.commands.GetKiCadBinaryPath";
|
||||||
|
pub(crate) const CMD_GET_PLUGIN_SETTINGS_PATH: &str = "kiapi.common.commands.GetPluginSettingsPath";
|
||||||
|
pub(crate) const CMD_GET_NET_CLASSES: &str = "kiapi.common.commands.GetNetClasses";
|
||||||
|
pub(crate) const CMD_SET_NET_CLASSES: &str = "kiapi.common.commands.SetNetClasses";
|
||||||
|
pub(crate) const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables";
|
||||||
|
pub(crate) const CMD_SET_TEXT_VARIABLES: &str = "kiapi.common.commands.SetTextVariables";
|
||||||
|
pub(crate) const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables";
|
||||||
|
pub(crate) const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents";
|
||||||
|
pub(crate) const CMD_GET_TEXT_AS_SHAPES: &str = "kiapi.common.commands.GetTextAsShapes";
|
||||||
|
pub(crate) const CMD_REFRESH_EDITOR: &str = "kiapi.common.commands.RefreshEditor";
|
||||||
|
pub(crate) const CMD_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocuments";
|
||||||
|
pub(crate) const CMD_RUN_ACTION: &str = "kiapi.common.commands.RunAction";
|
||||||
|
pub(crate) const CMD_GET_NETS: &str = "kiapi.board.commands.GetNets";
|
||||||
|
pub(crate) const CMD_GET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.GetBoardEnabledLayers";
|
||||||
|
pub(crate) const CMD_SET_BOARD_ENABLED_LAYERS: &str = "kiapi.board.commands.SetBoardEnabledLayers";
|
||||||
|
pub(crate) const CMD_GET_ACTIVE_LAYER: &str = "kiapi.board.commands.GetActiveLayer";
|
||||||
|
pub(crate) const CMD_SET_ACTIVE_LAYER: &str = "kiapi.board.commands.SetActiveLayer";
|
||||||
|
pub(crate) const CMD_GET_VISIBLE_LAYERS: &str = "kiapi.board.commands.GetVisibleLayers";
|
||||||
|
pub(crate) const CMD_SET_VISIBLE_LAYERS: &str = "kiapi.board.commands.SetVisibleLayers";
|
||||||
|
pub(crate) const CMD_GET_BOARD_LAYER_NAME: &str = "kiapi.board.commands.GetBoardLayerName";
|
||||||
|
pub(crate) const CMD_GET_BOARD_ORIGIN: &str = "kiapi.board.commands.GetBoardOrigin";
|
||||||
|
pub(crate) const CMD_SET_BOARD_ORIGIN: &str = "kiapi.board.commands.SetBoardOrigin";
|
||||||
|
pub(crate) const CMD_GET_BOARD_STACKUP: &str = "kiapi.board.commands.GetBoardStackup";
|
||||||
|
pub(crate) const CMD_UPDATE_BOARD_STACKUP: &str = "kiapi.board.commands.UpdateBoardStackup";
|
||||||
|
pub(crate) const CMD_GET_GRAPHICS_DEFAULTS: &str = "kiapi.board.commands.GetGraphicsDefaults";
|
||||||
|
pub(crate) const CMD_GET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||||
|
"kiapi.board.commands.GetBoardEditorAppearanceSettings";
|
||||||
|
pub(crate) const CMD_SET_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||||
|
"kiapi.board.commands.SetBoardEditorAppearanceSettings";
|
||||||
|
pub(crate) const CMD_INTERACTIVE_MOVE_ITEMS: &str = "kiapi.board.commands.InteractiveMoveItems";
|
||||||
|
pub(crate) const CMD_GET_ITEMS_BY_NET: &str = "kiapi.board.commands.GetItemsByNet";
|
||||||
|
pub(crate) const CMD_GET_ITEMS_BY_NET_CLASS: &str = "kiapi.board.commands.GetItemsByNetClass";
|
||||||
|
pub(crate) const CMD_GET_NETCLASS_FOR_NETS: &str = "kiapi.board.commands.GetNetClassForNets";
|
||||||
|
pub(crate) const CMD_REFILL_ZONES: &str = "kiapi.board.commands.RefillZones";
|
||||||
|
pub(crate) const CMD_GET_PAD_SHAPE_AS_POLYGON: &str = "kiapi.board.commands.GetPadShapeAsPolygon";
|
||||||
|
pub(crate) const CMD_CHECK_PADSTACK_PRESENCE_ON_LAYERS: &str =
|
||||||
|
"kiapi.board.commands.CheckPadstackPresenceOnLayers";
|
||||||
|
pub(crate) const CMD_INJECT_DRC_ERROR: &str = "kiapi.board.commands.InjectDrcError";
|
||||||
|
pub(crate) const CMD_GET_SELECTION: &str = "kiapi.common.commands.GetSelection";
|
||||||
|
pub(crate) const CMD_ADD_TO_SELECTION: &str = "kiapi.common.commands.AddToSelection";
|
||||||
|
pub(crate) const CMD_REMOVE_FROM_SELECTION: &str = "kiapi.common.commands.RemoveFromSelection";
|
||||||
|
pub(crate) const CMD_CLEAR_SELECTION: &str = "kiapi.common.commands.ClearSelection";
|
||||||
|
pub(crate) const CMD_BEGIN_COMMIT: &str = "kiapi.common.commands.BeginCommit";
|
||||||
|
pub(crate) const CMD_END_COMMIT: &str = "kiapi.common.commands.EndCommit";
|
||||||
|
pub(crate) const CMD_CREATE_ITEMS: &str = "kiapi.common.commands.CreateItems";
|
||||||
|
pub(crate) const CMD_UPDATE_ITEMS: &str = "kiapi.common.commands.UpdateItems";
|
||||||
|
pub(crate) const CMD_DELETE_ITEMS: &str = "kiapi.common.commands.DeleteItems";
|
||||||
|
pub(crate) const CMD_PARSE_AND_CREATE_ITEMS_FROM_STRING: &str =
|
||||||
|
"kiapi.common.commands.ParseAndCreateItemsFromString";
|
||||||
|
pub(crate) const CMD_GET_ITEMS: &str = "kiapi.common.commands.GetItems";
|
||||||
|
pub(crate) const CMD_GET_ITEMS_BY_ID: &str = "kiapi.common.commands.GetItemsById";
|
||||||
|
pub(crate) const CMD_GET_BOUNDING_BOX: &str = "kiapi.common.commands.GetBoundingBox";
|
||||||
|
pub(crate) const CMD_HIT_TEST: &str = "kiapi.common.commands.HitTest";
|
||||||
|
pub(crate) const CMD_GET_TITLE_BLOCK_INFO: &str = "kiapi.common.commands.GetTitleBlockInfo";
|
||||||
|
pub(crate) const CMD_SAVE_DOCUMENT: &str = "kiapi.common.commands.SaveDocument";
|
||||||
|
pub(crate) const CMD_SAVE_COPY_OF_DOCUMENT: &str = "kiapi.common.commands.SaveCopyOfDocument";
|
||||||
|
pub(crate) const CMD_REVERT_DOCUMENT: &str = "kiapi.common.commands.RevertDocument";
|
||||||
|
pub(crate) const CMD_SAVE_DOCUMENT_TO_STRING: &str = "kiapi.common.commands.SaveDocumentToString";
|
||||||
|
pub(crate) const CMD_SAVE_SELECTION_TO_STRING: &str = "kiapi.common.commands.SaveSelectionToString";
|
||||||
|
|
||||||
|
pub(crate) const RES_GET_VERSION: &str = "kiapi.common.commands.GetVersionResponse";
|
||||||
|
pub(crate) const RES_PATH_RESPONSE: &str = "kiapi.common.commands.PathResponse";
|
||||||
|
pub(crate) const RES_STRING_RESPONSE: &str = "kiapi.common.commands.StringResponse";
|
||||||
|
pub(crate) const RES_NET_CLASSES_RESPONSE: &str = "kiapi.common.commands.NetClassesResponse";
|
||||||
|
pub(crate) const RES_TEXT_VARIABLES: &str = "kiapi.common.project.TextVariables";
|
||||||
|
pub(crate) const RES_EXPAND_TEXT_VARIABLES_RESPONSE: &str =
|
||||||
|
"kiapi.common.commands.ExpandTextVariablesResponse";
|
||||||
|
pub(crate) const RES_BOX2: &str = "kiapi.common.types.Box2";
|
||||||
|
pub(crate) const RES_GET_TEXT_AS_SHAPES_RESPONSE: &str =
|
||||||
|
"kiapi.common.commands.GetTextAsShapesResponse";
|
||||||
|
pub(crate) const RES_GET_OPEN_DOCUMENTS: &str = "kiapi.common.commands.GetOpenDocumentsResponse";
|
||||||
|
pub(crate) const RES_RUN_ACTION_RESPONSE: &str = "kiapi.common.commands.RunActionResponse";
|
||||||
|
pub(crate) const RES_GET_NETS: &str = "kiapi.board.commands.NetsResponse";
|
||||||
|
pub(crate) const RES_GET_BOARD_ENABLED_LAYERS: &str =
|
||||||
|
"kiapi.board.commands.BoardEnabledLayersResponse";
|
||||||
|
pub(crate) const RES_BOARD_LAYER_RESPONSE: &str = "kiapi.board.commands.BoardLayerResponse";
|
||||||
|
pub(crate) const RES_BOARD_LAYERS: &str = "kiapi.board.commands.BoardLayers";
|
||||||
|
pub(crate) const RES_BOARD_LAYER_NAME_RESPONSE: &str =
|
||||||
|
"kiapi.board.commands.BoardLayerNameResponse";
|
||||||
|
pub(crate) const RES_BOARD_STACKUP_RESPONSE: &str = "kiapi.board.commands.BoardStackupResponse";
|
||||||
|
pub(crate) const RES_GRAPHICS_DEFAULTS_RESPONSE: &str =
|
||||||
|
"kiapi.board.commands.GraphicsDefaultsResponse";
|
||||||
|
pub(crate) const RES_BOARD_EDITOR_APPEARANCE_SETTINGS: &str =
|
||||||
|
"kiapi.board.commands.BoardEditorAppearanceSettings";
|
||||||
|
pub(crate) const RES_NETCLASS_FOR_NETS_RESPONSE: &str =
|
||||||
|
"kiapi.board.commands.NetClassForNetsResponse";
|
||||||
|
pub(crate) const RES_PAD_SHAPE_AS_POLYGON_RESPONSE: &str =
|
||||||
|
"kiapi.board.commands.PadShapeAsPolygonResponse";
|
||||||
|
pub(crate) const RES_PADSTACK_PRESENCE_RESPONSE: &str =
|
||||||
|
"kiapi.board.commands.PadstackPresenceResponse";
|
||||||
|
pub(crate) const RES_INJECT_DRC_ERROR_RESPONSE: &str =
|
||||||
|
"kiapi.board.commands.InjectDrcErrorResponse";
|
||||||
|
pub(crate) const RES_VECTOR2: &str = "kiapi.common.types.Vector2";
|
||||||
|
pub(crate) const RES_SELECTION_RESPONSE: &str = "kiapi.common.commands.SelectionResponse";
|
||||||
|
pub(crate) const RES_BEGIN_COMMIT_RESPONSE: &str = "kiapi.common.commands.BeginCommitResponse";
|
||||||
|
pub(crate) const RES_END_COMMIT_RESPONSE: &str = "kiapi.common.commands.EndCommitResponse";
|
||||||
|
pub(crate) const RES_CREATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.CreateItemsResponse";
|
||||||
|
pub(crate) const RES_UPDATE_ITEMS_RESPONSE: &str = "kiapi.common.commands.UpdateItemsResponse";
|
||||||
|
pub(crate) const RES_DELETE_ITEMS_RESPONSE: &str = "kiapi.common.commands.DeleteItemsResponse";
|
||||||
|
pub(crate) const RES_GET_ITEMS_RESPONSE: &str = "kiapi.common.commands.GetItemsResponse";
|
||||||
|
pub(crate) const RES_GET_BOUNDING_BOX_RESPONSE: &str =
|
||||||
|
"kiapi.common.commands.GetBoundingBoxResponse";
|
||||||
|
pub(crate) const RES_HIT_TEST_RESPONSE: &str = "kiapi.common.commands.HitTestResponse";
|
||||||
|
pub(crate) const RES_TITLE_BLOCK_INFO: &str = "kiapi.common.types.TitleBlockInfo";
|
||||||
|
pub(crate) const RES_SAVED_DOCUMENT_RESPONSE: &str = "kiapi.common.commands.SavedDocumentResponse";
|
||||||
|
pub(crate) const RES_SAVED_SELECTION_RESPONSE: &str =
|
||||||
|
"kiapi.common.commands.SavedSelectionResponse";
|
||||||
|
pub(crate) const RES_PROTOBUF_EMPTY: &str = "google.protobuf.Empty";
|
||||||
|
|
||||||
|
pub(crate) const PAD_QUERY_CHUNK_SIZE: usize = 256;
|
||||||
|
|
||||||
|
pub(crate) static PCB_OBJECT_TYPES: [PcbObjectTypeCode; 18] = [
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbFootprint as i32,
|
||||||
|
name: "KOT_PCB_FOOTPRINT",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbPad as i32,
|
||||||
|
name: "KOT_PCB_PAD",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbShape as i32,
|
||||||
|
name: "KOT_PCB_SHAPE",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbReferenceImage as i32,
|
||||||
|
name: "KOT_PCB_REFERENCE_IMAGE",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbField as i32,
|
||||||
|
name: "KOT_PCB_FIELD",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbGenerator as i32,
|
||||||
|
name: "KOT_PCB_GENERATOR",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbText as i32,
|
||||||
|
name: "KOT_PCB_TEXT",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbTextbox as i32,
|
||||||
|
name: "KOT_PCB_TEXTBOX",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbTable as i32,
|
||||||
|
name: "KOT_PCB_TABLE",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbTablecell as i32,
|
||||||
|
name: "KOT_PCB_TABLECELL",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbTrace as i32,
|
||||||
|
name: "KOT_PCB_TRACE",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbVia as i32,
|
||||||
|
name: "KOT_PCB_VIA",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbArc as i32,
|
||||||
|
name: "KOT_PCB_ARC",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbMarker as i32,
|
||||||
|
name: "KOT_PCB_MARKER",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbDimension as i32,
|
||||||
|
name: "KOT_PCB_DIMENSION",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbZone as i32,
|
||||||
|
name: "KOT_PCB_ZONE",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbGroup as i32,
|
||||||
|
name: "KOT_PCB_GROUP",
|
||||||
|
},
|
||||||
|
PcbObjectTypeCode {
|
||||||
|
code: common_types::KiCadObjectType::KotPcbBarcode as i32,
|
||||||
|
name: "KOT_PCB_BARCODE",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
/// Async IPC client for communicating with a running KiCad instance.
|
||||||
|
///
|
||||||
|
/// Create with [`KiCadClient::connect`] for defaults or [`KiCadClient::builder`]
|
||||||
|
/// to override socket path, timeout, token, or client name.
|
||||||
|
pub struct KiCadClient {
|
||||||
|
inner: Arc<ClientInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct ClientInner {
|
||||||
|
pub(crate) transport: Transport,
|
||||||
|
pub(crate) token: Mutex<String>,
|
||||||
|
pub(crate) client_name: String,
|
||||||
|
pub(crate) timeout: Duration,
|
||||||
|
pub(crate) socket_uri: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct ClientConfig {
|
||||||
|
timeout: Duration,
|
||||||
|
socket_uri: Option<String>,
|
||||||
|
token: Option<String>,
|
||||||
|
client_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
/// Builder for [`KiCadClient`].
|
||||||
|
///
|
||||||
|
/// Defaults:
|
||||||
|
/// - timeout: `3s`
|
||||||
|
/// - socket path: `KICAD_API_SOCKET` env var, then platform default
|
||||||
|
/// - token: `KICAD_API_TOKEN` env var, then empty
|
||||||
|
/// - client name: autogenerated
|
||||||
|
pub struct ClientBuilder {
|
||||||
|
config: ClientConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClientBuilder {
|
||||||
|
/// Creates a builder with sensible defaults for local KiCad IPC usage.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
config: ClientConfig {
|
||||||
|
timeout: Duration::from_millis(3_000),
|
||||||
|
socket_uri: None,
|
||||||
|
token: None,
|
||||||
|
client_name: None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets per-request timeout used by the IPC transport.
|
||||||
|
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||||
|
self.config.timeout = timeout;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets explicit KiCad IPC socket URI/path.
|
||||||
|
///
|
||||||
|
/// If unset, the builder resolves from environment/defaults.
|
||||||
|
pub fn socket_path(mut self, socket_path: impl Into<String>) -> Self {
|
||||||
|
self.config.socket_uri = Some(socket_path.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the IPC authentication token.
|
||||||
|
///
|
||||||
|
/// If unset, the builder uses `KICAD_API_TOKEN` when present.
|
||||||
|
pub fn token(mut self, token: impl Into<String>) -> Self {
|
||||||
|
self.config.token = Some(token.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the client name reported to KiCad.
|
||||||
|
pub fn client_name(mut self, client_name: impl Into<String>) -> Self {
|
||||||
|
self.config.client_name = Some(client_name.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects to KiCad IPC with the configured options.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns [`KiCadError`] when socket discovery, connection, or transport
|
||||||
|
/// initialization fails.
|
||||||
|
pub async fn connect(self) -> Result<KiCadClient, KiCadError> {
|
||||||
|
let socket_uri = resolve_socket_uri(self.config.socket_uri.as_deref());
|
||||||
|
if is_missing_ipc_socket(&socket_uri) {
|
||||||
|
return Err(KiCadError::SocketUnavailable { socket_uri });
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeout = self.config.timeout;
|
||||||
|
let transport = Transport::connect(&socket_uri, timeout)?;
|
||||||
|
|
||||||
|
let token = self
|
||||||
|
.config
|
||||||
|
.token
|
||||||
|
.or_else(|| std::env::var(KICAD_API_TOKEN_ENV).ok())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let client_name = self.config.client_name.unwrap_or_else(default_client_name);
|
||||||
|
|
||||||
|
Ok(KiCadClient {
|
||||||
|
inner: Arc::new(ClientInner {
|
||||||
|
transport,
|
||||||
|
token: Mutex::new(token),
|
||||||
|
client_name,
|
||||||
|
timeout,
|
||||||
|
socket_uri,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for ClientBuilder {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Returns a configurable builder for creating a [`KiCadClient`].
|
||||||
|
pub fn builder() -> ClientBuilder {
|
||||||
|
ClientBuilder::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Connects with default builder settings.
|
||||||
|
pub async fn connect() -> Result<Self, KiCadError> {
|
||||||
|
ClientBuilder::new().connect().await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the per-request timeout configured for this client.
|
||||||
|
pub fn timeout(&self) -> Duration {
|
||||||
|
self.inner.timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the IPC socket URI/path used by this client.
|
||||||
|
pub fn socket_uri(&self) -> &str {
|
||||||
|
&self.inner.socket_uri
|
||||||
|
}
|
||||||
|
pub(crate) async fn send_command(
|
||||||
|
&self,
|
||||||
|
command: prost_types::Any,
|
||||||
|
) -> Result<crate::proto::kiapi::common::ApiResponse, KiCadError> {
|
||||||
|
let token = self
|
||||||
|
.inner
|
||||||
|
.token
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| KiCadError::InternalPoisoned)?
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let request_bytes = envelope::encode_request(&token, &self.inner.client_name, command)?;
|
||||||
|
let response_bytes = self.inner.transport.roundtrip(request_bytes).await?;
|
||||||
|
|
||||||
|
let response = envelope::decode_response(&response_bytes)?;
|
||||||
|
|
||||||
|
if let Some(err) = envelope::status_error(&response) {
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
if token.is_empty() {
|
||||||
|
if let Some(header) = response.header.as_ref() {
|
||||||
|
if !header.kicad_token.is_empty() {
|
||||||
|
let mut guard = self
|
||||||
|
.inner
|
||||||
|
.token
|
||||||
|
.lock()
|
||||||
|
.map_err(|_| KiCadError::InternalPoisoned)?;
|
||||||
|
*guard = header.kicad_token.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn current_board_document_proto(
|
||||||
|
&self,
|
||||||
|
) -> Result<common_types::DocumentSpecifier, KiCadError> {
|
||||||
|
let docs = self.get_open_documents(DocumentType::Pcb).await?;
|
||||||
|
let selected = select_single_board_document(&docs)?;
|
||||||
|
Ok(model_document_to_proto(selected))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn current_board_item_header(
|
||||||
|
&self,
|
||||||
|
) -> Result<common_types::ItemHeader, KiCadError> {
|
||||||
|
Ok(common_types::ItemHeader {
|
||||||
|
document: Some(self.current_board_document_proto().await?),
|
||||||
|
container: None,
|
||||||
|
field_mask: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_items_raw(
|
||||||
|
&self,
|
||||||
|
types: Vec<i32>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = common_commands::GetItems {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
types,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_ITEMS))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::GetItemsResponse =
|
||||||
|
envelope::unpack_any(&response, RES_GET_ITEMS_RESPONSE)?;
|
||||||
|
|
||||||
|
ensure_item_request_ok(payload.status)?;
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn map_document_specifier(
|
||||||
|
source: common_types::DocumentSpecifier,
|
||||||
|
) -> Option<DocumentSpecifier> {
|
||||||
|
let document_type = DocumentType::from_proto(source.r#type)?;
|
||||||
|
let board_filename = match source.identifier {
|
||||||
|
Some(common_types::document_specifier::Identifier::BoardFilename(filename)) => {
|
||||||
|
Some(filename)
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = source.project.unwrap_or_default();
|
||||||
|
|
||||||
|
let project_info = ProjectInfo {
|
||||||
|
name: if project.name.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(project.name)
|
||||||
|
},
|
||||||
|
path: if project.path.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(PathBuf::from(project.path))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(DocumentSpecifier {
|
||||||
|
document_type,
|
||||||
|
board_filename,
|
||||||
|
project: project_info,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_single_board_document(
|
||||||
|
docs: &[DocumentSpecifier],
|
||||||
|
) -> Result<&DocumentSpecifier, KiCadError> {
|
||||||
|
if docs.is_empty() {
|
||||||
|
return Err(KiCadError::BoardNotOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if docs.len() > 1 {
|
||||||
|
let boards = docs
|
||||||
|
.iter()
|
||||||
|
.map(|doc| {
|
||||||
|
doc.board_filename
|
||||||
|
.clone()
|
||||||
|
.unwrap_or_else(|| "<unknown>".to_string())
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return Err(KiCadError::AmbiguousBoardSelection { boards });
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(&docs[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn select_single_project_path(
|
||||||
|
docs: &[DocumentSpecifier],
|
||||||
|
) -> Result<PathBuf, KiCadError> {
|
||||||
|
let mut paths = BTreeSet::new();
|
||||||
|
for doc in docs {
|
||||||
|
if let Some(path) = doc.project.path.as_ref() {
|
||||||
|
paths.insert(path.display().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if paths.is_empty() {
|
||||||
|
return Err(KiCadError::BoardNotOpen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if paths.len() > 1 {
|
||||||
|
return Err(KiCadError::AmbiguousProjectPath {
|
||||||
|
paths: paths.into_iter().collect(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = paths.into_iter().next().ok_or(KiCadError::BoardNotOpen)?;
|
||||||
|
Ok(PathBuf::from(first))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolve_current_project_path(
|
||||||
|
docs_result: Result<Vec<DocumentSpecifier>, KiCadError>,
|
||||||
|
) -> Result<PathBuf, KiCadError> {
|
||||||
|
match docs_result {
|
||||||
|
Ok(docs) => select_single_project_path(&docs),
|
||||||
|
Err(err) if is_get_open_documents_unhandled(&err) => {
|
||||||
|
project_path_from_environment().ok_or(err)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project_path_from_environment() -> Option<PathBuf> {
|
||||||
|
let value = std::env::var(KIPRJMOD_ENV).ok()?;
|
||||||
|
let trimmed = value.trim();
|
||||||
|
if trimmed.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(PathBuf::from(trimmed))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn is_get_open_documents_unhandled(err: &KiCadError) -> bool {
|
||||||
|
matches!(
|
||||||
|
err,
|
||||||
|
KiCadError::ApiStatus { code, .. } if code == "AS_UNHANDLED"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_socket_uri(explicit: Option<&str>) -> String {
|
||||||
|
if let Some(socket) = explicit {
|
||||||
|
return normalize_socket_uri(socket);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Ok(socket) = std::env::var(KICAD_API_SOCKET_ENV) {
|
||||||
|
if !socket.is_empty() {
|
||||||
|
return normalize_socket_uri(&socket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
normalize_socket_uri(default_socket_path().to_string_lossy().as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_socket_path() -> PathBuf {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
return std::env::temp_dir().join("kicad").join("api.sock");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
{
|
||||||
|
if let Some(home) = std::env::var_os("HOME") {
|
||||||
|
let flatpak = PathBuf::from(home)
|
||||||
|
.join(".var")
|
||||||
|
.join("app")
|
||||||
|
.join("org.kicad.KiCad")
|
||||||
|
.join("cache")
|
||||||
|
.join("tmp")
|
||||||
|
.join("kicad")
|
||||||
|
.join("api.sock");
|
||||||
|
if flatpak.exists() {
|
||||||
|
return flatpak;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathBuf::from("/tmp/kicad/api.sock")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn normalize_socket_uri(socket: &str) -> String {
|
||||||
|
if socket.contains("://") {
|
||||||
|
return socket.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
format!("ipc://{socket}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ipc_path_from_uri(socket_uri: &str) -> Option<PathBuf> {
|
||||||
|
let raw_path = socket_uri.strip_prefix("ipc://")?;
|
||||||
|
Some(PathBuf::from(raw_path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_missing_ipc_socket(socket_uri: &str) -> bool {
|
||||||
|
if let Some(path) = ipc_path_from_uri(socket_uri) {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
// On Windows, nng's ipc:// transport uses named pipes, not filesystem
|
||||||
|
// sockets. A path.exists() check is always false even when KiCad is
|
||||||
|
// running. Instead, probe the named pipe directly: a successful open
|
||||||
|
// or ERROR_PIPE_BUSY (231) both mean a server is listening.
|
||||||
|
let pipe_path = format!(r"\\.\pipe\{}", path.display());
|
||||||
|
return match std::fs::OpenOptions::new().read(true).open(&pipe_path) {
|
||||||
|
Ok(_) => false,
|
||||||
|
Err(e) => e.raw_os_error() != Some(231),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "windows"))]
|
||||||
|
return !path.exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_client_name() -> String {
|
||||||
|
let millis = SystemTime::now()
|
||||||
|
.duration_since(UNIX_EPOCH)
|
||||||
|
.map(|duration| duration.as_millis())
|
||||||
|
.unwrap_or(0);
|
||||||
|
|
||||||
|
format!("kicad-ipc-{}-{millis}", std::process::id())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
//! Selection management: get, add, remove, and clear the active PCB selection.
|
||||||
|
|
||||||
|
use crate::envelope;
|
||||||
|
use crate::error::KiCadError;
|
||||||
|
use crate::model::board::*;
|
||||||
|
use crate::model::common::*;
|
||||||
|
use crate::proto::kiapi::common::commands as common_commands;
|
||||||
|
use crate::proto::kiapi::common::types as common_types;
|
||||||
|
|
||||||
|
use super::decode::*;
|
||||||
|
use super::mappers::*;
|
||||||
|
use super::{
|
||||||
|
KiCadClient, CMD_ADD_TO_SELECTION, CMD_CLEAR_SELECTION, CMD_GET_SELECTION,
|
||||||
|
CMD_REMOVE_FROM_SELECTION, RES_PROTOBUF_EMPTY, RES_SELECTION_RESPONSE,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl KiCadClient {
|
||||||
|
/// Returns summarized counts for the current selection, optionally filtered by type codes.
|
||||||
|
pub async fn get_selection_summary(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
) -> Result<SelectionSummary, KiCadError> {
|
||||||
|
let document = self.current_board_document_proto().await?;
|
||||||
|
let command = common_commands::GetSelection {
|
||||||
|
header: Some(common_types::ItemHeader {
|
||||||
|
document: Some(document),
|
||||||
|
container: None,
|
||||||
|
field_mask: None,
|
||||||
|
}),
|
||||||
|
types: type_codes,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_SELECTION))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::SelectionResponse =
|
||||||
|
envelope::unpack_any(&response, RES_SELECTION_RESPONSE)?;
|
||||||
|
|
||||||
|
Ok(summarize_selection(&payload.items))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns current selection items as raw protobuf payloads.
|
||||||
|
pub async fn get_selection_raw(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = common_commands::GetSelection {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
types: type_codes,
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_GET_SELECTION))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let payload: common_commands::SelectionResponse =
|
||||||
|
envelope::unpack_any(&response, RES_SELECTION_RESPONSE)?;
|
||||||
|
|
||||||
|
Ok(payload.items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns lightweight detail rows for the current selection.
|
||||||
|
pub async fn get_selection_details(
|
||||||
|
&self,
|
||||||
|
type_codes: Vec<i32>,
|
||||||
|
) -> Result<Vec<SelectionItemDetail>, KiCadError> {
|
||||||
|
let items = self.get_selection_raw(type_codes).await?;
|
||||||
|
summarize_item_details(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current selection as decoded typed PCB items.
|
||||||
|
pub async fn get_selection(&self, type_codes: Vec<i32>) -> Result<Vec<PcbItem>, KiCadError> {
|
||||||
|
let items = self.get_selection_raw(type_codes).await?;
|
||||||
|
decode_pcb_items(items)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds item ids to the current selection and returns raw selection payloads.
|
||||||
|
pub async fn add_to_selection_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = common_commands::AddToSelection {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_ADD_TO_SELECTION))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match envelope::unpack_any::<common_commands::SelectionResponse>(
|
||||||
|
&response,
|
||||||
|
RES_SELECTION_RESPONSE,
|
||||||
|
) {
|
||||||
|
Ok(payload) => Ok(payload.items),
|
||||||
|
Err(KiCadError::UnexpectedPayloadType {
|
||||||
|
expected_type_url: _,
|
||||||
|
actual_type_url,
|
||||||
|
}) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds item ids to the current selection and returns typed items with summary.
|
||||||
|
pub async fn add_to_selection(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<SelectionMutationResult, KiCadError> {
|
||||||
|
let raw_items = self.add_to_selection_raw(item_ids).await?;
|
||||||
|
let summary = summarize_selection(&raw_items);
|
||||||
|
let items = decode_pcb_items(raw_items)?;
|
||||||
|
Ok(SelectionMutationResult { items, summary })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the current selection and returns raw selection payloads.
|
||||||
|
pub async fn clear_selection_raw(&self) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = common_commands::ClearSelection {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_CLEAR_SELECTION))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match envelope::unpack_any::<common_commands::SelectionResponse>(
|
||||||
|
&response,
|
||||||
|
RES_SELECTION_RESPONSE,
|
||||||
|
) {
|
||||||
|
Ok(payload) => Ok(payload.items),
|
||||||
|
Err(KiCadError::UnexpectedPayloadType {
|
||||||
|
expected_type_url: _,
|
||||||
|
actual_type_url,
|
||||||
|
}) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears the current selection and returns typed items with summary.
|
||||||
|
pub async fn clear_selection(&self) -> Result<SelectionMutationResult, KiCadError> {
|
||||||
|
let raw_items = self.clear_selection_raw().await?;
|
||||||
|
let summary = summarize_selection(&raw_items);
|
||||||
|
let items = decode_pcb_items(raw_items)?;
|
||||||
|
Ok(SelectionMutationResult { items, summary })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes item ids from the current selection and returns raw selection payloads.
|
||||||
|
pub async fn remove_from_selection_raw(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<Vec<prost_types::Any>, KiCadError> {
|
||||||
|
let command = common_commands::RemoveFromSelection {
|
||||||
|
header: Some(self.current_board_item_header().await?),
|
||||||
|
items: item_ids
|
||||||
|
.into_iter()
|
||||||
|
.map(|value| common_types::Kiid { value })
|
||||||
|
.collect(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = self
|
||||||
|
.send_command(envelope::pack_any(&command, CMD_REMOVE_FROM_SELECTION))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match envelope::unpack_any::<common_commands::SelectionResponse>(
|
||||||
|
&response,
|
||||||
|
RES_SELECTION_RESPONSE,
|
||||||
|
) {
|
||||||
|
Ok(payload) => Ok(payload.items),
|
||||||
|
Err(KiCadError::UnexpectedPayloadType {
|
||||||
|
expected_type_url: _,
|
||||||
|
actual_type_url,
|
||||||
|
}) if actual_type_url == envelope::type_url(RES_PROTOBUF_EMPTY) => Ok(Vec::new()),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes item ids from the current selection and returns typed items with summary.
|
||||||
|
pub async fn remove_from_selection(
|
||||||
|
&self,
|
||||||
|
item_ids: Vec<String>,
|
||||||
|
) -> Result<SelectionMutationResult, KiCadError> {
|
||||||
|
let raw_items = self.remove_from_selection_raw(item_ids).await?;
|
||||||
|
let summary = summarize_selection(&raw_items);
|
||||||
|
let items = decode_pcb_items(raw_items)?;
|
||||||
|
Ok(SelectionMutationResult { items, summary })
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,2 +1,3 @@
|
||||||
// Generated by tools/proto-gen.
|
// Generated by tools/proto-gen.
|
||||||
|
/// KiCad API revision used to generate vendored protobuf bindings.
|
||||||
pub const KICAD_API_VERSION: &str = "10.0.0-0-g0feeca2a";
|
pub const KICAD_API_VERSION: &str = "10.0.0-0-g0feeca2a";
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
|
|
||||||
/// High-level async client and request/response convenience methods.
|
/// High-level async client and request/response convenience methods.
|
||||||
|
#[allow(clippy::module_inception)]
|
||||||
pub mod client;
|
pub mod client;
|
||||||
/// Low-level command payload builders.
|
/// Low-level command payload builders.
|
||||||
///
|
///
|
||||||
|
|
@ -68,9 +69,11 @@ pub mod commands;
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub mod envelope;
|
pub mod envelope;
|
||||||
/// Error types returned by this crate.
|
/// Error types returned by this crate.
|
||||||
|
#[allow(missing_docs)]
|
||||||
pub mod error;
|
pub mod error;
|
||||||
mod kicad_api_version;
|
mod kicad_api_version;
|
||||||
/// Stable data models used by typed client APIs.
|
/// Stable data models used by typed client APIs.
|
||||||
|
#[allow(missing_docs)]
|
||||||
pub mod model;
|
pub mod model;
|
||||||
/// IPC transport implementation details.
|
/// IPC transport implementation details.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Generated protobuf code can trigger clippy::enum_variant_names due to schema-defined naming.
|
||||||
|
// Allow this lint at the module level so it applies to all generated includes below.
|
||||||
|
#[allow(clippy::enum_variant_names)]
|
||||||
pub(crate) mod kiapi {
|
pub(crate) mod kiapi {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub mod common {
|
pub mod common {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue