@@ -2,9 +2,7 @@ use anyhow::{Context, Result as AnyhowResult};
22use log:: { debug, info} ;
33use pcb_sch:: { AttributeValue , InstanceKind , Schematic , ATTR_LAYOUT_PATH } ;
44use pcb_zen_core:: diagnostics:: Diagnostic ;
5- use pcb_zen_core:: lang:: stackup:: {
6- ApproxEq , BoardConfig , BoardConfigError , NetClass , Stackup , StackupError , THICKNESS_EPS ,
7- } ;
5+ use pcb_zen_core:: lang:: stackup:: { BoardConfig , BoardConfigError , NetClass , Stackup , StackupError } ;
86use rust_decimal:: prelude:: ToPrimitive ;
97use serde:: { Deserialize , Serialize } ;
108use starlark:: errors:: EvalSeverity ;
@@ -601,7 +599,7 @@ pub fn process_layout(
601599 // Apply board config (stackup + netclass patterns)
602600 if let Some ( ref config) = board_config {
603601 if let Some ( ref stackup) = config. stackup {
604- patch_stackup_if_needed ( & paths. pcb , stackup) ?;
602+ patch_stackup ( & paths. pcb , stackup) ?;
605603 }
606604
607605 let assignments = build_netclass_assignments ( schematic, config. netclasses ( ) ) ;
@@ -940,189 +938,142 @@ fn patch_netclass_patterns(
940938 Ok ( ( ) )
941939}
942940
943- /// Apply stackup configuration if it differs from existing PCB file
944- fn patch_stackup_if_needed ( pcb_path : & Path , zen_stackup : & Stackup ) -> Result < ( ) , LayoutError > {
945- // Read current PCB file
941+ /// Apply and normalize stackup-related sections in a PCB file.
942+ fn patch_stackup ( pcb_path : & Path , zen_stackup : & Stackup ) -> Result < ( ) , LayoutError > {
946943 let pcb_content = fs:: read_to_string ( pcb_path) . map_err ( |e| {
947944 LayoutError :: StackupPatchingError ( format ! ( "Failed to read PCB file: {}" , e) )
948945 } ) ?;
949946
950- // Parse existing stackup from PCB file
951- let existing_stackup = Stackup :: from_kicad_pcb ( & pcb_content) ?;
952-
953- // Compare stackups - only patch if they're different
954- let needs_update = match existing_stackup {
955- Some ( existing) => {
956- let equivalent = zen_stackup. approx_eq ( & existing, THICKNESS_EPS ) ;
957- if !equivalent {
958- debug ! ( "Zen stackup: {:?}" , zen_stackup) ;
959- debug ! ( "Existing stackup: {:?}" , existing) ;
960- }
961- !equivalent
962- }
963- None => {
964- debug ! ( "No existing stackup found in PCB file" ) ;
965- true // No existing stackup, so we need to add it
966- }
967- } ;
968-
969- if !needs_update {
970- debug ! ( "Stackup configuration matches, skipping update" ) ;
971- return Ok ( ( ) ) ;
972- }
947+ let mut board = pcb_sexpr:: parse ( & pcb_content) . map_err ( |e| {
948+ LayoutError :: StackupPatchingError ( format ! ( "Failed to parse PCB file: {}" , e) )
949+ } ) ?;
973950
974- info ! ( "Updating stackup configuration in {}" , pcb_path. display( ) ) ;
951+ let layers = zen_stackup. generate_layers_expr ( 4 ) ;
952+ let stackup = zen_stackup. generate_stackup_expr ( ) ;
975953
976- // Generate new S-expressions (using default user layers)
977- let layers_sexpr = zen_stackup. generate_layers_sexpr ( 4 ) ;
978- let stackup_sexpr = zen_stackup. generate_stackup_sexpr ( ) ;
954+ apply_stackup_sections ( & mut board, layers, stackup) ?;
979955
980- // Use surgical string replacement to avoid parsing issues with hex numbers
981- let mut updated_content = pcb_content;
982- updated_content = replace_section_in_pcb_content ( & updated_content, "layers" , & layers_sexpr) ?;
983- updated_content = replace_section_in_pcb_content ( & updated_content, "stackup" , & stackup_sexpr) ?;
956+ let updated_content =
957+ pcb_sexpr:: formatter:: format_tree ( & board, pcb_sexpr:: formatter:: FormatMode :: Normal ) ;
984958
985- // Write updated content back to file
959+ info ! ( "Updating stackup configuration in {}" , pcb_path . display ( ) ) ;
986960 fs:: write ( pcb_path, updated_content) . map_err ( |e| {
987961 LayoutError :: StackupPatchingError ( format ! ( "Failed to write updated PCB file: {}" , e) )
988962 } ) ?;
989-
990963 info ! ( "Successfully updated stackup configuration" ) ;
964+
991965 Ok ( ( ) )
992966}
993967
994- /// Replace a section in KiCad PCB content using careful string matching
995- fn replace_section_in_pcb_content (
996- content : & str ,
997- section_name : & str ,
998- new_section : & str ,
999- ) -> Result < String , LayoutError > {
1000- // Find the section by parsing just enough to locate it
1001- let section_start = find_section_start ( content, section_name) ?;
1002-
1003- if let Some ( start_pos) = section_start {
1004- let end_pos = find_matching_paren ( content, start_pos) ?;
1005-
1006- // Replace the section with the new content
1007- let mut result = String :: with_capacity ( content. len ( ) + new_section. len ( ) ) ;
1008- result. push_str ( & content[ ..start_pos] ) ;
1009- result. push_str ( new_section) ;
1010- result. push_str ( & content[ end_pos + 1 ..] ) ;
1011- Ok ( result)
1012- } else {
1013- // Section doesn't exist, need to add it
1014- add_section_to_pcb_content ( content, section_name, new_section)
968+ fn apply_stackup_sections (
969+ board : & mut pcb_sexpr:: Sexpr ,
970+ layers : pcb_sexpr:: Sexpr ,
971+ stackup : pcb_sexpr:: Sexpr ,
972+ ) -> Result < ( ) , LayoutError > {
973+ let root_items = board. as_list_mut ( ) . ok_or_else ( || {
974+ LayoutError :: StackupPatchingError ( "PCB root is not an S-expression list" . to_string ( ) )
975+ } ) ?;
976+
977+ if root_items. first ( ) . and_then ( pcb_sexpr:: Sexpr :: as_sym) != Some ( "kicad_pcb" ) {
978+ return Err ( LayoutError :: StackupPatchingError (
979+ "PCB root must start with (kicad_pcb ...)" . to_string ( ) ,
980+ ) ) ;
1015981 }
1016- }
1017982
1018- /// Find the start position of a section in PCB content
1019- fn find_section_start ( content : & str , section_name : & str ) -> Result < Option < usize > , LayoutError > {
1020- let pattern = format ! ( "({}" , section_name) ;
1021- let mut pos = 0 ;
983+ set_or_insert_named_list ( root_items, "layers" , layers, Some ( "general" ) ) ;
1022984
1023- while let Some ( found) = content[ pos..] . find ( & pattern) {
1024- let abs_pos = pos + found;
985+ if let Some ( setup_idx) = find_named_list_index ( root_items, "setup" ) {
986+ let setup_items = root_items[ setup_idx] . as_list_mut ( ) . ok_or_else ( || {
987+ LayoutError :: StackupPatchingError ( "setup section is not a list" . to_string ( ) )
988+ } ) ?;
989+ set_or_insert_named_list ( setup_items, "stackup" , stackup, None ) ;
990+ } else {
991+ root_items. push ( pcb_sexpr:: Sexpr :: list ( vec ! [
992+ pcb_sexpr:: Sexpr :: symbol( "setup" ) ,
993+ stackup,
994+ ] ) ) ;
995+ }
1025996
1026- // Check if this is a word boundary (not part of a larger identifier)
1027- let next_char_pos = abs_pos + pattern. len ( ) ;
1028- if next_char_pos < content. len ( ) {
1029- let next_char = content. chars ( ) . nth ( next_char_pos) . unwrap ( ) ;
1030- if next_char. is_whitespace ( ) || next_char == '\n' || next_char == '\t' {
1031- return Ok ( Some ( abs_pos) ) ;
1032- }
1033- } else {
1034- return Ok ( Some ( abs_pos) ) ;
1035- }
997+ Ok ( ( ) )
998+ }
999+
1000+ fn find_named_list_index ( items : & [ pcb_sexpr:: Sexpr ] , name : & str ) -> Option < usize > {
1001+ items. iter ( ) . position ( |item| {
1002+ item. as_list ( )
1003+ . and_then ( |list| list. first ( ) )
1004+ . and_then ( pcb_sexpr:: Sexpr :: as_sym)
1005+ == Some ( name)
1006+ } )
1007+ }
10361008
1037- pos = abs_pos + 1 ;
1009+ fn set_or_insert_named_list (
1010+ parent : & mut Vec < pcb_sexpr:: Sexpr > ,
1011+ name : & str ,
1012+ replacement : pcb_sexpr:: Sexpr ,
1013+ insert_after : Option < & str > ,
1014+ ) {
1015+ if let Some ( idx) = find_named_list_index ( parent, name) {
1016+ parent[ idx] = replacement;
1017+ return ;
10381018 }
10391019
1040- Ok ( None )
1020+ let insert_idx = insert_after
1021+ . and_then ( |anchor| find_named_list_index ( parent, anchor) . map ( |idx| idx + 1 ) )
1022+ . unwrap_or ( parent. len ( ) ) ;
1023+
1024+ parent. insert ( insert_idx, replacement) ;
10411025}
10421026
1043- /// Find the matching closing parenthesis for an opening parenthesis
1044- fn find_matching_paren ( content : & str , start_pos : usize ) -> Result < usize , LayoutError > {
1045- let mut depth = 0 ;
1046- let mut in_string = false ;
1047- let mut escaped = false ;
1027+ #[ cfg( test) ]
1028+ mod tests {
1029+ use super :: apply_stackup_sections;
1030+
1031+ #[ test]
1032+ fn apply_stackup_sections_replaces_existing_layers_and_stackup ( ) {
1033+ let mut board = pcb_sexpr:: parse (
1034+ r#"(kicad_pcb
1035+ (general (thickness 1.6))
1036+ (layers (0 "F.Cu" signal))
1037+ (setup (stackup (old yes)) (other 1))
1038+ )"# ,
1039+ )
1040+ . unwrap ( ) ;
10481041
1049- let chars: Vec < char > = content. chars ( ) . collect ( ) ;
1042+ let new_layers =
1043+ pcb_sexpr:: parse ( r#"(layers (0 "F.Cu" signal) (2 "B.Cu" signal))"# ) . unwrap ( ) ;
1044+ let new_stackup = pcb_sexpr:: parse ( r#"(stackup (layer "F.Cu" (type "copper")))"# ) . unwrap ( ) ;
10501045
1051- for ( i, & ch) in chars. iter ( ) . enumerate ( ) . skip ( start_pos) {
1052- if escaped {
1053- escaped = false ;
1054- continue ;
1055- }
1046+ apply_stackup_sections ( & mut board, new_layers, new_stackup) . unwrap ( ) ;
10561047
1057- match ch {
1058- '\\' if in_string => escaped = true ,
1059- '"' => in_string = !in_string,
1060- '(' if !in_string => depth += 1 ,
1061- ')' if !in_string => {
1062- depth -= 1 ;
1063- if depth == 0 {
1064- return Ok ( i) ;
1065- }
1066- }
1067- _ => { }
1068- }
1048+ let formatted =
1049+ pcb_sexpr:: formatter:: format_tree ( & board, pcb_sexpr:: formatter:: FormatMode :: Normal ) ;
1050+ assert ! ( formatted. contains( r#"(2 "B.Cu" signal)"# ) ) ;
1051+ assert ! ( formatted. contains( r#"(layer "F.Cu""# ) ) ;
1052+ assert ! ( formatted. contains( r#"(type "copper")"# ) ) ;
1053+ assert ! ( !formatted. contains( "(old yes)" ) ) ;
10691054 }
10701055
1071- Err ( LayoutError :: StackupPatchingError (
1072- "Could not find matching closing parenthesis" . to_string ( ) ,
1073- ) )
1074- }
1056+ #[ test]
1057+ fn apply_stackup_sections_inserts_layers_and_setup_when_missing ( ) {
1058+ let mut board =
1059+ pcb_sexpr:: parse ( r#"(kicad_pcb (version 20240101) (general (thickness 1.6)))"# )
1060+ . unwrap ( ) ;
10751061
1076- /// Add a new section to PCB content
1077- fn add_section_to_pcb_content (
1078- content : & str ,
1079- section_name : & str ,
1080- new_section : & str ,
1081- ) -> Result < String , LayoutError > {
1082- match section_name {
1083- "layers" => {
1084- // Add after general section
1085- if let Some ( general_start) = find_section_start ( content, "general" ) ? {
1086- let general_end = find_matching_paren ( content, general_start) ?;
1087- let insert_pos = general_end + 1 ;
1088-
1089- let mut result = String :: with_capacity ( content. len ( ) + new_section. len ( ) + 10 ) ;
1090- result. push_str ( & content[ ..insert_pos] ) ;
1091- result. push ( '\n' ) ;
1092- result. push ( '\t' ) ;
1093- result. push_str ( new_section) ;
1094- result. push_str ( & content[ insert_pos..] ) ;
1095- Ok ( result)
1096- } else {
1097- Err ( LayoutError :: StackupPatchingError (
1098- "Could not find general section for layers insertion" . to_string ( ) ,
1099- ) )
1100- }
1101- }
1102- "stackup" => {
1103- // Add within setup section
1104- if let Some ( setup_start) = find_section_start ( content, "setup" ) ? {
1105- let setup_end = find_matching_paren ( content, setup_start) ?;
1106- let insert_pos = setup_end; // Before closing paren
1107-
1108- let mut result = String :: with_capacity ( content. len ( ) + new_section. len ( ) + 20 ) ;
1109- result. push_str ( & content[ ..insert_pos] ) ;
1110- result. push ( '\n' ) ;
1111- result. push_str ( "\t \t " ) ;
1112- result. push_str ( new_section) ;
1113- result. push ( '\n' ) ;
1114- result. push ( '\t' ) ;
1115- result. push_str ( & content[ insert_pos..] ) ;
1116- Ok ( result)
1117- } else {
1118- Err ( LayoutError :: StackupPatchingError (
1119- "Could not find setup section for stackup insertion" . to_string ( ) ,
1120- ) )
1121- }
1122- }
1123- _ => Err ( LayoutError :: StackupPatchingError ( format ! (
1124- "Unknown section type: {}" ,
1125- section_name
1126- ) ) ) ,
1062+ let new_layers =
1063+ pcb_sexpr:: parse ( r#"(layers (0 "F.Cu" signal) (2 "B.Cu" signal))"# ) . unwrap ( ) ;
1064+ let new_stackup = pcb_sexpr:: parse ( r#"(stackup (layer "F.Cu" (type "copper")))"# ) . unwrap ( ) ;
1065+
1066+ apply_stackup_sections ( & mut board, new_layers, new_stackup) . unwrap ( ) ;
1067+
1068+ let formatted =
1069+ pcb_sexpr:: formatter:: format_tree ( & board, pcb_sexpr:: formatter:: FormatMode :: Normal ) ;
1070+ assert ! ( formatted. contains( "\n \t (layers\n " ) ) ;
1071+ assert ! ( formatted. contains( "\n \t (setup\n " ) ) ;
1072+ assert ! ( formatted. contains( r#"(layer "F.Cu""# ) ) ;
1073+ assert ! ( formatted. contains( r#"(type "copper")"# ) ) ;
1074+
1075+ let general_pos = formatted. find ( "\n \t (general" ) . unwrap ( ) ;
1076+ let layers_pos = formatted. find ( "\n \t (layers" ) . unwrap ( ) ;
1077+ assert ! ( layers_pos > general_pos) ;
11271078 }
11281079}
0 commit comments