diff --git a/README.md b/README.md index 4cae709..e684c5b 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ Deferred manual/runtime verification (implemented after 2026-02-20 while user un - `UpdateItems` - `DeleteItems` - `ParseAndCreateItemsFromString` +- `SetNetClasses` ## KiCad v10 RC1.1 API Completion Matrix @@ -64,10 +65,10 @@ Legend: | --- | ---: | ---: | ---: | | Common (base) | 6 | 6 | 100% | | Common editor/document | 23 | 23 | 100% | -| Project manager | 5 | 3 | 60% | +| Project manager | 5 | 4 | 80% | | Board editor (PCB) | 22 | 20 | 91% | | Schematic editor (dedicated proto commands) | 0 | 0 | n/a | -| **Total** | **56** | **52** | **93%** | +| **Total** | **56** | **53** | **95%** | ### Common (base) @@ -113,7 +114,7 @@ Legend: | KiCad Command | Status | Rust API | | --- | --- | --- | | `GetNetClasses` | Implemented | `KiCadClient::get_net_classes_raw`, `KiCadClient::get_net_classes` | -| `SetNetClasses` | Not yet | - | +| `SetNetClasses` | Implemented | `KiCadClient::set_net_classes_raw`, `KiCadClient::set_net_classes` | | `ExpandTextVariables` | Implemented | `KiCadClient::expand_text_variables_raw`, `KiCadClient::expand_text_variables` | | `GetTextVariables` | Implemented | `KiCadClient::get_text_variables_raw`, `KiCadClient::get_text_variables` | | `SetTextVariables` | Not yet | - | diff --git a/docs/TEST_CLI.md b/docs/TEST_CLI.md index 4a9931a..5370e61 100644 --- a/docs/TEST_CLI.md +++ b/docs/TEST_CLI.md @@ -65,6 +65,12 @@ List project net classes: cargo run --bin kicad-ipc-cli -- net-classes ``` +Write current net classes back with selected merge mode: + +```bash +cargo run --bin kicad-ipc-cli -- set-net-classes --merge-mode merge +``` + List text variables for current board document: ```bash diff --git a/src/client.rs b/src/client.rs index 1763d7d..5f63cd4 100644 --- a/src/client.rs +++ b/src/client.rs @@ -19,10 +19,10 @@ use crate::model::board::{ }; use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, - ItemHitTestResult, PcbObjectTypeCode, ProjectInfo, RunActionStatus, SelectionItemDetail, - SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, - TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, - TextVerticalAlignment, TitleBlockInfo, VersionInfo, + ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, ProjectInfo, RunActionStatus, + SelectionItemDetail, SelectionSummary, SelectionTypeCount, TextAsShapesEntry, + TextAttributesSpec, TextBoxSpec, TextExtents, TextHorizontalAlignment, TextObjectSpec, + TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; use crate::proto::kiapi::board as board_proto; use crate::proto::kiapi::board::commands as board_commands; @@ -40,6 +40,7 @@ const CMD_GET_VERSION: &str = "kiapi.common.commands.GetVersion"; const CMD_GET_KICAD_BINARY_PATH: &str = "kiapi.common.commands.GetKiCadBinaryPath"; const CMD_GET_PLUGIN_SETTINGS_PATH: &str = "kiapi.common.commands.GetPluginSettingsPath"; const CMD_GET_NET_CLASSES: &str = "kiapi.common.commands.GetNetClasses"; +const CMD_SET_NET_CLASSES: &str = "kiapi.common.commands.SetNetClasses"; const CMD_GET_TEXT_VARIABLES: &str = "kiapi.common.commands.GetTextVariables"; const CMD_EXPAND_TEXT_VARIABLES: &str = "kiapi.common.commands.ExpandTextVariables"; const CMD_GET_TEXT_EXTENTS: &str = "kiapi.common.commands.GetTextExtents"; @@ -465,6 +466,33 @@ impl KiCadClient { Ok(classes) } + pub async fn set_net_classes_raw( + &self, + net_classes: Vec, + merge_mode: MapMergeMode, + ) -> Result { + 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) + } + + pub async fn set_net_classes( + &self, + net_classes: Vec, + merge_mode: MapMergeMode, + ) -> Result, KiCadError> { + let _ = self.set_net_classes_raw(net_classes, merge_mode).await?; + self.get_net_classes().await + } + pub async fn get_text_variables_raw(&self) -> Result { let command = common_commands::GetTextVariables { document: Some(self.current_board_document_proto().await?), @@ -2161,6 +2189,13 @@ fn commit_action_to_proto(action: CommitAction) -> i32 { } } +fn map_merge_mode_to_proto(value: MapMergeMode) -> i32 { + match value { + MapMergeMode::Merge => common_types::MapMergeMode::MmmMerge as i32, + MapMergeMode::Replace => common_types::MapMergeMode::MmmReplace as i32, + } +} + fn summarize_selection(items: Vec) -> SelectionSummary { let mut counts = BTreeMap::::new(); @@ -2645,6 +2680,62 @@ fn board_editor_appearance_settings_to_proto( } } +fn net_class_type_to_proto(value: NetClassType) -> i32 { + match value { + NetClassType::Explicit => common_project::NetClassType::NctExplicit as i32, + NetClassType::Implicit => common_project::NetClassType::NctImplicit as i32, + NetClassType::Unknown(raw) => raw, + } +} + +fn net_class_info_to_proto(value: NetClassInfo) -> common_project::NetClass { + let board = value + .board + .map(|board| common_project::NetClassBoardSettings { + clearance: board + .clearance_nm + .map(|value_nm| common_types::Distance { value_nm }), + track_width: board + .track_width_nm + .map(|value_nm| common_types::Distance { value_nm }), + diff_pair_track_width: board + .diff_pair_track_width_nm + .map(|value_nm| common_types::Distance { value_nm }), + diff_pair_gap: board + .diff_pair_gap_nm + .map(|value_nm| common_types::Distance { value_nm }), + diff_pair_via_gap: board + .diff_pair_via_gap_nm + .map(|value_nm| common_types::Distance { value_nm }), + via_stack: if board.has_via_stack { + Some(board_types::PadStack::default()) + } else { + None + }, + microvia_stack: if board.has_microvia_stack { + Some(board_types::PadStack::default()) + } else { + None + }, + color: board.color.map(|color| common_types::Color { + r: color.r, + g: color.g, + b: color.b, + a: color.a, + }), + tuning_profile: board.tuning_profile, + }); + + common_project::NetClass { + name: value.name, + priority: value.priority, + board, + schematic: None, + r#type: net_class_type_to_proto(value.class_type), + constituents: value.constituents, + } +} + fn map_net_class_type(value: i32) -> NetClassType { match common_project::NetClassType::try_from(value) { Ok(common_project::NetClassType::NctExplicit) => NetClassType::Explicit, @@ -3379,11 +3470,12 @@ mod tests { any_to_pretty_debug, board_editor_appearance_settings_to_proto, commit_action_to_proto, drc_severity_to_proto, ensure_item_deletion_status_ok, ensure_item_request_ok, ensure_item_status_ok, layer_to_model, map_commit_session, map_hit_test_result, - map_item_bounding_boxes, map_polygon_with_holes, map_run_action_status, - model_document_to_proto, normalize_socket_uri, pad_netlist_from_footprint_items, - response_payload_as_any, select_single_board_document, select_single_project_path, - selection_item_detail, summarize_item_details, summarize_selection, - text_horizontal_alignment_to_proto, text_spec_to_proto, PCB_OBJECT_TYPES, + map_item_bounding_boxes, map_merge_mode_to_proto, map_polygon_with_holes, + map_run_action_status, model_document_to_proto, normalize_socket_uri, + pad_netlist_from_footprint_items, response_payload_as_any, select_single_board_document, + select_single_project_path, selection_item_detail, summarize_item_details, + summarize_selection, text_horizontal_alignment_to_proto, text_spec_to_proto, + PCB_OBJECT_TYPES, }; use crate::error::KiCadError; use crate::model::common::{ @@ -3551,6 +3643,18 @@ mod tests { ); } + #[test] + fn map_merge_mode_to_proto_maps_known_variants() { + assert_eq!( + map_merge_mode_to_proto(crate::model::common::MapMergeMode::Merge), + crate::proto::kiapi::common::types::MapMergeMode::MmmMerge as i32 + ); + assert_eq!( + map_merge_mode_to_proto(crate::model::common::MapMergeMode::Replace), + crate::proto::kiapi::common::types::MapMergeMode::MmmReplace as i32 + ); + } + #[test] fn drc_severity_to_proto_maps_known_variants() { assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index de9a66d..5f4b03a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,8 +34,8 @@ pub use crate::model::board::{ }; pub use crate::model::common::{ CommitAction, CommitSession, DocumentSpecifier, DocumentType, EditorFrameType, ItemBoundingBox, - ItemHitTestResult, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, SelectionSummary, - SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, TextExtents, - TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, + ItemHitTestResult, MapMergeMode, PcbObjectTypeCode, RunActionStatus, SelectionItemDetail, + SelectionSummary, SelectionTypeCount, TextAsShapesEntry, TextAttributesSpec, TextBoxSpec, + TextExtents, TextHorizontalAlignment, TextObjectSpec, TextShape, TextShapeGeometry, TextSpec, TextVerticalAlignment, TitleBlockInfo, VersionInfo, }; diff --git a/src/model/common.rs b/src/model/common.rs index 1c12352..745b0e3 100644 --- a/src/model/common.rs +++ b/src/model/common.rs @@ -191,6 +191,35 @@ pub enum RunActionStatus { Unknown(i32), } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum MapMergeMode { + Merge, + Replace, +} + +impl std::fmt::Display for MapMergeMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Merge => write!(f, "merge"), + Self::Replace => write!(f, "replace"), + } + } +} + +impl FromStr for MapMergeMode { + type Err = String; + + fn from_str(value: &str) -> Result { + match value { + "merge" => Ok(Self::Merge), + "replace" => Ok(Self::Replace), + _ => Err(format!( + "unknown merge mode `{value}`; expected `merge` or `replace`" + )), + } + } +} + impl std::fmt::Display for CommitAction { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { @@ -403,7 +432,7 @@ impl std::fmt::Display for ItemHitTestResult { #[cfg(test)] mod tests { - use super::{CommitAction, EditorFrameType}; + use super::{CommitAction, EditorFrameType, MapMergeMode}; use std::str::FromStr; #[test] @@ -433,4 +462,15 @@ mod tests { fn editor_frame_type_rejects_unknown_values() { assert!(EditorFrameType::from_str("layout").is_err()); } + + #[test] + fn map_merge_mode_parses_known_values() { + assert_eq!(MapMergeMode::from_str("merge"), Ok(MapMergeMode::Merge)); + assert_eq!(MapMergeMode::from_str("replace"), Ok(MapMergeMode::Replace)); + } + + #[test] + fn map_merge_mode_rejects_unknown_values() { + assert!(MapMergeMode::from_str("upsert").is_err()); + } } diff --git a/test-scripts/kicad-ipc-cli.rs b/test-scripts/kicad-ipc-cli.rs index fe5edb0..bfabeec 100644 --- a/test-scripts/kicad-ipc-cli.rs +++ b/test-scripts/kicad-ipc-cli.rs @@ -7,7 +7,7 @@ use std::time::Duration; use kicad_ipc::{ BoardFlipMode, BoardOriginKind, ClientBuilder, CommitAction, CommitSession, DocumentType, - DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, + DrcSeverity, EditorFrameType, InactiveLayerDisplayMode, KiCadClient, KiCadError, MapMergeMode, NetColorDisplayMode, PadstackPresenceState, PcbObjectTypeCode, RatsnestDisplayMode, TextObjectSpec, TextShapeGeometry, TextSpec, Vector2Nm, }; @@ -42,6 +42,9 @@ enum Command { ProjectPath, BoardOpen, NetClasses, + SetNetClasses { + merge_mode: MapMergeMode, + }, TextVariables, ExpandTextVariables { text: Vec, @@ -296,6 +299,15 @@ async fn run() -> Result<(), KiCadError> { ); } } + Command::SetNetClasses { merge_mode } => { + let classes = client.get_net_classes().await?; + let updated = client.set_net_classes(classes, merge_mode).await?; + println!( + "net_class_count={} merge_mode={}", + updated.len(), + merge_mode + ); + } Command::TextVariables => { let variables = client.get_text_variables().await?; println!("text_variable_count={}", variables.len()); @@ -961,6 +973,23 @@ fn parse_args_from(mut args: Vec) -> Result<(CliConfig, Command), KiCadE "project-path" => Command::ProjectPath, "board-open" => Command::BoardOpen, "net-classes" => Command::NetClasses, + "set-net-classes" => { + let mut merge_mode = MapMergeMode::Merge; + let mut i = 1; + while i < args.len() { + if args[i] == "--merge-mode" { + let value = args.get(i + 1).ok_or_else(|| KiCadError::Config { + reason: "missing value for set-net-classes --merge-mode".to_string(), + })?; + merge_mode = MapMergeMode::from_str(value) + .map_err(|reason| KiCadError::Config { reason })?; + i += 2; + continue; + } + i += 1; + } + Command::SetNetClasses { merge_mode } + } "text-variables" => Command::TextVariables, "expand-text-variables" => { let mut text = Vec::new(); @@ -1965,7 +1994,7 @@ fn default_config() -> CliConfig { fn print_help() { println!( - "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n kicad-binary-path [--binary-name ]\n Resolve absolute path for a KiCad binary (default: kicad-cli)\n plugin-settings-path [--identifier ]\n Resolve writeable plugin settings directory (default: kicad-ipc-rust)\n open-docs [--type ] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n net-classes List project netclass definitions\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text (repeatable)\n text-extents Measure text bounding box\n Options: --text \n text-as-shapes Convert text to rendered shapes\n Options: --text (repeatable)\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display \n --net-color-display \n --board-flip \n --ratsnest-display \n Set editor appearance settings\n inject-drc-error --severity --message [--x-nm --y-nm ] [--item-id ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n refill-zones [--zone-id ...]\n Refill all zones or a provided subset\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count [--layer-id ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id \n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id ...\n Set visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n set-board-origin --type --x-nm --y-nm \n Set board origin (`grid` or `drill`)\n refresh-editor [--frame ] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id [--action ] [--message ]\n End staged commit with commit/drop action\n save-doc Save current board document\n save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\n add-to-selection --id ...\n Add items to current selection\n remove-from-selection --id ...\n Remove items from current selection\n clear-selection Clear current item selection\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" + "kicad-ipc-cli\n\nUSAGE:\n cargo run --bin kicad-ipc-cli -- [--socket URI] [--token TOKEN] [--client-name NAME] [--timeout-ms N] [command options]\n\nCOMMANDS:\n ping Check IPC connectivity\n version Fetch KiCad version\n kicad-binary-path [--binary-name ]\n Resolve absolute path for a KiCad binary (default: kicad-cli)\n plugin-settings-path [--identifier ]\n Resolve writeable plugin settings directory (default: kicad-ipc-rust)\n open-docs [--type ] List open docs (default type: pcb)\n project-path Get current project path from open PCB docs\n board-open Exit non-zero if no PCB doc is open\n net-classes List project netclass definitions\n set-net-classes [--merge-mode ]\n Write current netclass set back with selected merge mode\n text-variables List text variables for current board document\n expand-text-variables Expand variables in provided text values\n Options: --text (repeatable)\n text-extents Measure text bounding box\n Options: --text \n text-as-shapes Convert text to rendered shapes\n Options: --text (repeatable)\n nets List board nets (requires one open PCB)\n netlist-pads Emit pad-level netlist data (with footprint context)\n items-by-id --id ... Show parsed details for specific item IDs\n item-bbox --id ... Show bounding boxes for item IDs\n hit-test --id --x-nm --y-nm [--tolerance-nm ]\n Hit-test one item at a point\n types-pcb List PCB KiCad object type IDs from proto enum\n items-raw --type-id ... Dump raw Any payloads for requested item type IDs\n items-raw-all-pcb [--debug] Dump all PCB item payloads across all PCB object types\n pad-shape-polygon --pad-id ... --layer-id [--debug]\n Dump pad polygons on a target layer\n padstack-presence --item-id ... --layer-id ... [--debug]\n Check padstack shape presence matrix across layers\n title-block Show title block fields\n board-as-string Dump board as KiCad s-expression text\n selection-as-string Dump current selection as KiCad s-expression text\n stackup Show typed board stackup\n graphics-defaults Show typed graphics defaults\n appearance Show typed editor appearance settings\n set-appearance --inactive-layer-display \n --net-color-display \n --board-flip \n --ratsnest-display \n Set editor appearance settings\n inject-drc-error --severity --message [--x-nm --y-nm ] [--item-id ...]\n Inject a DRC marker (severity: warning|error|exclusion|ignore|info|action|debug|undefined)\n refill-zones [--zone-id ...]\n Refill all zones or a provided subset\n netclass Show typed netclass map for current board nets\n proto-coverage-board-read Print board-read command coverage vs proto\n board-read-report [--out P] Write markdown board reconstruction report\n enabled-layers List enabled board layers\n set-enabled-layers --copper-layer-count [--layer-id ...]\n Set enabled board layer set\n active-layer Show active board layer\n set-active-layer --layer-id \n Set active board layer\n visible-layers Show currently visible board layers\n set-visible-layers --layer-id ...\n Set visible board layers\n board-origin [--type ] Show board origin (`grid` default, or `drill`)\n set-board-origin --type --x-nm --y-nm \n Set board origin (`grid` or `drill`)\n refresh-editor [--frame ] Refresh a specific editor frame (default: pcb)\n begin-commit Start staged commit and print commit ID\n end-commit --id [--action ] [--message ]\n End staged commit with commit/drop action\n save-doc Save current board document\n save-copy --path [--overwrite] [--include-project]\n Save current board document to a new location\n revert-doc Revert current board document from disk\n run-action --action Run a raw KiCad tool action\n create-items --item = ... [--container-id ]\n Create raw Any payload items in current board document\n update-items --item = ...\n Update raw Any payload items in current board document\n delete-items --id ...\n Delete item IDs from current board document\n parse-create-items --contents \n Parse s-expression and create resulting items\n add-to-selection --id ...\n Add items to current selection\n remove-from-selection --id ...\n Remove items from current selection\n clear-selection Clear current item selection\n selection-summary Show current selection item type counts\n selection-details Show parsed details for selected items\n selection-raw Show raw Any payload bytes for selected items\n smoke ping + version + board-open summary\n help Show help\n\nTYPES:\n schematic | symbol | pcb | footprint | drawing-sheet | project\n" ); } @@ -2699,6 +2728,23 @@ mod tests { } } + #[test] + fn parse_args_parses_set_net_classes() { + let (_, command) = parse_args_from(vec![ + "set-net-classes".to_string(), + "--merge-mode".to_string(), + "replace".to_string(), + ]) + .expect("set-net-classes args should parse"); + + match command { + Command::SetNetClasses { merge_mode } => { + assert_eq!(merge_mode, kicad_ipc::MapMergeMode::Replace) + } + other => panic!("unexpected command variant: {other:?}"), + } + } + #[test] fn parse_args_parses_save_doc() { let (_, command) =