diff --git a/pcbnew/drc/drc_test_provider_solder_mask.cpp b/pcbnew/drc/drc_test_provider_solder_mask.cpp index 748723a1e4..9ada75cef0 100644 --- a/pcbnew/drc/drc_test_provider_solder_mask.cpp +++ b/pcbnew/drc/drc_test_provider_solder_mask.cpp @@ -97,8 +97,14 @@ private: // Shapes used to define solder mask apertures don't have nets, so we assign them the // first object+net that bridges their aperture (after which any other nets will generate // violations). + // + // When "report all track errors" is enabled, we store all items per net so we can report + // violations for each pair of items from different nets. std::mutex m_netMapMutex; std::unordered_map> m_maskApertureNetMap; + + // Extended storage for "report all track errors" mode: stores all items per net per aperture + std::unordered_map>> m_maskApertureNetMapAll; }; @@ -338,12 +344,13 @@ bool DRC_TEST_PROVIDER_SOLDER_MASK::checkMaskAperture( BOARD_ITEM* aMaskItem, BO int encounteredItemNet = -1; { - std::lock_guard lock( m_checkedPairsMutex ); + std::lock_guard lock( m_netMapMutex ); auto ii = m_maskApertureNetMap.find( key ); if( ii == m_maskApertureNetMap.end() ) { m_maskApertureNetMap[ key ] = { aTestItem, aTestNet }; + m_maskApertureNetMapAll[ key ].push_back( { aTestItem, aTestNet } ); // First net; no bridge yet.... return false; @@ -351,12 +358,15 @@ bool DRC_TEST_PROVIDER_SOLDER_MASK::checkMaskAperture( BOARD_ITEM* aMaskItem, BO alreadyEncounteredItem = ii->second.first; encounteredItemNet = ii->second.second; - } - if( encounteredItemNet == aTestNet && aTestNet >= 0 ) - { - // Same net; still no bridge... - return false; + if( encounteredItemNet == aTestNet && aTestNet >= 0 ) + { + // Same net; still no bridge, but add this item to the list so we can report all + // pairwise violations later (for non-tracks always, for tracks when option is set). + m_maskApertureNetMapAll[ key ].push_back( { aTestItem, aTestNet } ); + + return false; + } } if( fp && aTestItem->GetParentFootprint() == fp ) @@ -593,24 +603,106 @@ void DRC_TEST_PROVIDER_SOLDER_MASK::testItemAgainstItems( BOARD_ITEM* aItem, con { if( checkMaskAperture( aItem, other, aRefLayer, otherNet, &colliding ) ) { - std::shared_ptr drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE ); + PCB_LAYER_ID maskLayer = IsFrontLayer( aRefLayer ) ? F_Mask : B_Mask; + PTR_LAYER_CACHE_KEY key = { aItem, maskLayer }; + std::vector> itemsToReport; + bool reportedAnyTrack = false; - drce->SetErrorMessage( msg ); - drce->SetItems( aItem, colliding, other ); - drce->SetViolatingRule( &m_bridgeRule ); - reportViolation( drce, pos, aTargetLayer ); + { + std::lock_guard lock( m_netMapMutex ); + auto it = m_maskApertureNetMapAll.find( key ); + + if( it != m_maskApertureNetMapAll.end() ) + itemsToReport = it->second; + } + + if( !itemsToReport.empty() ) + { + for( auto& [firstNetItem, firstNet] : itemsToReport ) + { + // Always report all combinations for non-track items. + // For tracks, report all only when option is set; otherwise + // report just one track violation. + bool firstIsTrack = firstNetItem->Type() == PCB_TRACE_T + || firstNetItem->Type() == PCB_ARC_T; + + if( firstIsTrack ) + { + if( m_drcEngine->GetReportAllTrackErrors() || !reportedAnyTrack ) + { + auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE ); + + drce->SetErrorMessage( msg ); + drce->SetItems( aItem, firstNetItem, other ); + drce->SetViolatingRule( &m_bridgeRule ); + reportViolation( drce, pos, aTargetLayer ); + reportedAnyTrack = true; + } + } + else + { + auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE ); + + drce->SetErrorMessage( msg ); + drce->SetItems( aItem, firstNetItem, other ); + drce->SetViolatingRule( &m_bridgeRule ); + reportViolation( drce, pos, aTargetLayer ); + } + } + } } } else if( isMaskAperture( other ) ) { if( checkMaskAperture( other, aItem, aRefLayer, itemNet, &colliding ) ) { - std::shared_ptr drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE ); + PCB_LAYER_ID maskLayer = IsFrontLayer( aRefLayer ) ? F_Mask : B_Mask; + PTR_LAYER_CACHE_KEY key = { other, maskLayer }; + std::vector> itemsToReport; + bool reportedAnyTrack = false; - drce->SetErrorMessage( msg ); - drce->SetItems( other, colliding, aItem ); - drce->SetViolatingRule( &m_bridgeRule ); - reportViolation( drce, pos, aTargetLayer ); + { + std::lock_guard lock( m_netMapMutex ); + auto it = m_maskApertureNetMapAll.find( key ); + + if( it != m_maskApertureNetMapAll.end() ) + itemsToReport = it->second; + } + + if( !itemsToReport.empty() ) + { + for( auto& [firstNetItem, firstNet] : itemsToReport ) + { + // Always report all combinations for non-track items. + // For tracks, report all only when option is set; otherwise + // report just one track violation. + bool firstIsTrack = firstNetItem->Type() == PCB_TRACE_T + || firstNetItem->Type() == PCB_ARC_T; + + if( firstIsTrack ) + { + if( m_drcEngine->GetReportAllTrackErrors() || !reportedAnyTrack ) + { + auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE ); + + drce->SetErrorMessage( msg ); + drce->SetItems( other, firstNetItem, aItem ); + drce->SetViolatingRule( &m_bridgeRule ); + reportViolation( drce, pos, aTargetLayer ); + reportedAnyTrack = true; + } + } + else + { + auto drce = DRC_ITEM::Create( DRCE_SOLDERMASK_BRIDGE ); + + drce->SetErrorMessage( msg ); + drce->SetItems( other, firstNetItem, aItem ); + drce->SetViolatingRule( &m_bridgeRule ); + reportViolation( drce, pos, aTargetLayer ); + } + } + } } } else if( checkItemMask( other, itemNet ) ) @@ -825,10 +917,16 @@ bool DRC_TEST_PROVIDER_SOLDER_MASK::Run() updateLargestClearance( static_cast( item )->GetSolderMaskExpansion() ); } - // Order is important here: m_webWidth must be added in before m_largestCourtyardClearance is - // maxed with the various SILK_CLEARANCE_CONSTRAINTS. + // Order is important here: m_webWidth must be added in before m_largestClearance is + // maxed with the various clearance constraints. m_largestClearance += m_largestClearance + m_webWidth; + // Include SolderMaskToCopperClearance so R-tree queries find copper items that are within + // the required distance of mask apertures. Without this, tracks passing near pad apertures + // from different nets would not be found if SolderMaskToCopperClearance > m_largestClearance. + m_largestClearance = std::max( m_largestClearance, + m_board->GetDesignSettings().m_SolderMaskToCopperClearance ); + DRC_CONSTRAINT worstClearanceConstraint; if( m_drcEngine->QueryWorstConstraint( SILK_CLEARANCE_CONSTRAINT, worstClearanceConstraint ) ) @@ -839,6 +937,7 @@ bool DRC_TEST_PROVIDER_SOLDER_MASK::Run() m_checkedPairs.clear(); m_maskApertureNetMap.clear(); + m_maskApertureNetMapAll.clear(); buildRTrees(); diff --git a/qa/data/pcbnew/soldermask_bridge_all_tracks.kicad_pcb b/qa/data/pcbnew/soldermask_bridge_all_tracks.kicad_pcb new file mode 100644 index 0000000000..849e0caad1 --- /dev/null +++ b/qa/data/pcbnew/soldermask_bridge_all_tracks.kicad_pcb @@ -0,0 +1,494 @@ +(kicad_pcb + (version 20241229) + (generator "pcbnew") + (generator_version "9.0") + (general + (thickness 1.6) + (legacy_teardrops no) + ) + (paper "A4") + (layers + (0 "F.Cu" signal) + (2 "B.Cu" signal) + (9 "F.Adhes" user "F.Adhesive") + (11 "B.Adhes" user "B.Adhesive") + (13 "F.Paste" user) + (15 "B.Paste" user) + (5 "F.SilkS" user "F.Silkscreen") + (7 "B.SilkS" user "B.Silkscreen") + (1 "F.Mask" user) + (3 "B.Mask" user) + (17 "Dwgs.User" user "User.Drawings") + (19 "Cmts.User" user "User.Comments") + (21 "Eco1.User" user "User.Eco1") + (23 "Eco2.User" user "User.Eco2") + (25 "Edge.Cuts" user) + (27 "Margin" user) + (31 "F.CrtYd" user "F.Courtyard") + (29 "B.CrtYd" user "B.Courtyard") + (35 "F.Fab" user) + (33 "B.Fab" user) + (39 "User.1" user) + (41 "User.2" user) + (43 "User.3" user) + (45 "User.4" user) + ) + (setup + (pad_to_mask_clearance 0) + (allow_soldermask_bridges_in_footprints no) + (tenting front back) + (pcbplotparams + (layerselection 0x00000000_00000000_55555555_5755f5ff) + (plot_on_all_layers_selection 0x00000000_00000000_00000000_00000000) + (disableapertmacros no) + (usegerberextensions no) + (usegerberattributes yes) + (usegerberadvancedattributes yes) + (creategerberjobfile yes) + (dashed_line_dash_ratio 12.000000) + (dashed_line_gap_ratio 3.000000) + (svgprecision 4) + (plotframeref no) + (mode 1) + (useauxorigin no) + (hpglpennumber 1) + (hpglpenspeed 20) + (hpglpendiameter 15.000000) + (pdf_front_fp_property_popups yes) + (pdf_back_fp_property_popups yes) + (pdf_metadata yes) + (pdf_single_document no) + (dxfpolygonmode yes) + (dxfimperialunits yes) + (dxfusepcbnewfont yes) + (psnegative no) + (psa4output no) + (plot_black_and_white yes) + (sketchpadsonfab no) + (plotpadnumbers no) + (hidednponfab no) + (sketchdnponfab yes) + (crossoutdnponfab yes) + (subtractmaskfromsilk no) + (outputformat 1) + (mirror no) + (drillshape 1) + (scaleselection 1) + (outputdirectory "") + ) + ) + (net 0 "") + (net 1 "a") + (net 2 "b") + (footprint "Diode_THT:D_DO-247_Horizontal_TabDown" + (layer "F.Cu") + (uuid "971fa951-d58e-4bc8-a611-d70210c90fdb") + (at 193 59) + (descr "Diode, DO-247 series, Horizontal, TabDown, P 10.9mm") + (tags "diode rectifier") + (property "Reference" "REF**" + (at 5.45 -27.15 0) + (layer "F.SilkS") + (uuid "69e8b83a-a18a-4019-9510-efc8d3af1181") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Value" "D_DO-247_Horizontal_TabDown" + (at 5.45 3.25 0) + (layer "F.Fab") + (uuid "971ad017-97b6-48dd-b0c2-194c7704bb15") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (property "Datasheet" "" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "2dbd617d-da63-4cf5-9e2a-874b00aa8703") + (effects + (font + (size 1.27 1.27) + (thickness 0.15) + ) + ) + ) + (property "Description" "" + (at 0 0 0) + (unlocked yes) + (layer "F.Fab") + (hide yes) + (uuid "5d166361-c5b7-42ae-a1ca-87ed9d397eb2") + (effects + (font + (size 1.27 1.27) + (thickness 0.15) + ) + ) + ) + (attr through_hole) + (fp_line + (start -2.62 -26.15) + (end -2.62 -4.96) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "c4d62f0c-6fc0-4dcf-a2c9-fe06fd47d17a") + ) + (fp_line + (start -2.62 -26.15) + (end 13.52 -26.15) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "7ea5aa52-0e4b-4173-8528-6594ff41b56f") + ) + (fp_line + (start -2.62 -4.96) + (end 13.52 -4.96) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "172f5657-2113-4425-b917-9d7ebdf77cd6") + ) + (fp_line + (start 0 -4.96) + (end 0 -2.4) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "42863e0b-8ca5-49a0-a75a-f0116870e4d0") + ) + (fp_line + (start 4 -2) + (end 4 2) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "ac8db9d5-57bd-400b-8bda-fdb5edc4936e") + ) + (fp_line + (start 4 0) + (end 2.5 0) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "1a7200a3-e719-496d-bae5-a9102bf5c1ff") + ) + (fp_line + (start 4 0) + (end 7 -2) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "bbe77901-6bd4-41ed-b18a-9575af40a908") + ) + (fp_line + (start 7 -2) + (end 7 2) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "6b7ef3ea-35b4-4bfe-80b8-d9092e529119") + ) + (fp_line + (start 7 0) + (end 8.5 0) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "e2c40f56-3085-4586-8366-225beb4e632e") + ) + (fp_line + (start 7 2) + (end 4 0) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "80c34c54-4634-4022-83c3-d4c5e0d60721") + ) + (fp_line + (start 10.9 -4.96) + (end 10.9 -2.4) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "b4a19923-cf34-4f8c-b522-f1920c0000c2") + ) + (fp_line + (start 13.52 -26.15) + (end 13.52 -4.96) + (stroke + (width 0.12) + (type solid) + ) + (layer "F.SilkS") + (uuid "7777916c-2224-489b-a07b-f4d824e6fd73") + ) + (fp_line + (start -2.75 -26.28) + (end -2.75 2.5) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "6f175f83-0773-4d39-8059-5aa81041cae0") + ) + (fp_line + (start -2.75 2.5) + (end 13.65 2.5) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "52abf367-bc48-4d1f-95b2-0df23c2a75db") + ) + (fp_line + (start 13.65 -26.28) + (end -2.75 -26.28) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "553c8292-61e7-45e4-a29f-23d47cbd5715") + ) + (fp_line + (start 13.65 2.5) + (end 13.65 -26.28) + (stroke + (width 0.05) + (type solid) + ) + (layer "F.CrtYd") + (uuid "db38a51c-e5c8-405a-a929-6d72d9c116b7") + ) + (fp_line + (start -2.5 -26.03) + (end 13.4 -26.03) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "26532d99-a1b6-4f84-a2e4-4d94ea49778d") + ) + (fp_line + (start -2.5 -5.08) + (end -2.5 -26.03) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "4005a834-d0ae-4d2a-ab6d-d4a7a43d98c6") + ) + (fp_line + (start 0 -5.08) + (end 0 0) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "59b9c817-6fde-4b2b-bb34-0c8ac031f799") + ) + (fp_line + (start 10.9 -5.08) + (end 10.9 0) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "6138197f-dd9c-4754-895f-8663cc8a09a9") + ) + (fp_line + (start 13.4 -26.03) + (end 13.4 -5.08) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "4906d6b3-80d5-43a4-a4e9-dadc9d92b909") + ) + (fp_line + (start 13.4 -5.08) + (end -2.5 -5.08) + (stroke + (width 0.1) + (type solid) + ) + (layer "F.Fab") + (uuid "33b114cb-9df0-4554-9b98-859bfa9e6e88") + ) + (fp_circle + (center 5.45 -19.86) + (end 7.255 -19.86) + (stroke + (width 0.1) + (type solid) + ) + (fill no) + (layer "F.Fab") + (uuid "fa956f5c-23b4-4dcb-a8fe-0e0db1f25ab1") + ) + (fp_text user "${REFERENCE}" + (at 5.45 -27.15 0) + (layer "F.Fab") + (uuid "8c3ea185-51a7-4385-a58e-63f88218ca0d") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + ) + ) + (pad "" np_thru_hole oval + (at 5.45 -19.86) + (size 3.6 3.6) + (drill 3.6) + (layers "*.Cu" "*.Mask") + (uuid "b51b6f62-3220-4f4d-90e0-8a7c41b6727a") + ) + (pad "1" thru_hole rect + (at 0 0) + (size 2.5 4.5) + (drill 1.5) + (layers "*.Cu" "*.Mask") + (remove_unused_layers no) + (net 1 "a") + (uuid "5ffcbe15-06fd-4537-a9ad-5a0150cab53e") + ) + (pad "2" thru_hole oval + (at 10.9 0) + (size 2.5 4.5) + (drill 1.5) + (layers "*.Cu" "*.Mask") + (remove_unused_layers no) + (net 2 "b") + (uuid "60096c5f-1518-45c1-84c6-56fbc3c2b033") + ) + (embedded_fonts no) + (model "${KICAD9_3DMODEL_DIR}/Diode_THT.3dshapes/D_DO-247_Horizontal_TabDown.step" + (offset + (xyz 0 0 0) + ) + (scale + (xyz 1 1 1) + ) + (rotate + (xyz 0 0 0) + ) + ) + ) + (gr_rect + (start 191 60) + (end 205 81) + (stroke + (width 0.1) + (type default) + ) + (fill yes) + (layer "F.Mask") + (uuid "4515b5a6-f517-4096-b718-e6099702aebd") + ) + (segment + (start 197 66) + (end 198 67) + (width 0.2) + (layer "F.Cu") + (net 1) + (uuid "2b83570d-6580-43c6-826a-b24b5c910270") + ) + (segment + (start 193 62) + (end 197 66) + (width 0.2) + (layer "F.Cu") + (net 1) + (uuid "5173d5c5-567f-4e68-a64d-53d8c68cf218") + ) + (segment + (start 193 59) + (end 193 62) + (width 0.2) + (layer "F.Cu") + (net 1) + (uuid "a38aeae3-381f-472a-b729-f6cb65dde676") + ) + (segment + (start 198 67) + (end 198 80) + (width 0.2) + (layer "F.Cu") + (net 1) + (uuid "b9c9b074-47d6-49f1-9bd3-2ac72b7ac9cf") + ) + (segment + (start 201 65) + (end 199 67) + (width 0.2) + (layer "F.Cu") + (net 2) + (uuid "1d9d967f-1c84-44f9-9e79-79599d939906") + ) + (segment + (start 203.9 62.1) + (end 201 65) + (width 0.2) + (layer "F.Cu") + (net 2) + (uuid "25704320-724b-4996-8cb7-38e5a448aaa1") + ) + (segment + (start 199 67) + (end 199 80) + (width 0.2) + (layer "F.Cu") + (net 2) + (uuid "37a4bd9a-91cf-42a9-aaa9-ef44d65e6333") + ) + (segment + (start 203.9 59) + (end 203.9 62.1) + (width 0.2) + (layer "F.Cu") + (net 2) + (uuid "daaab621-c991-425c-8d9e-0aaf32a89efa") + ) + (embedded_fonts no) +) diff --git a/qa/data/pcbnew/soldermask_bridge_all_tracks.kicad_pro b/qa/data/pcbnew/soldermask_bridge_all_tracks.kicad_pro new file mode 100644 index 0000000000..e279956858 --- /dev/null +++ b/qa/data/pcbnew/soldermask_bridge_all_tracks.kicad_pro @@ -0,0 +1,614 @@ +{ + "board": { + "3dviewports": [], + "design_settings": { + "defaults": { + "apply_defaults_to_fp_fields": false, + "apply_defaults_to_fp_shapes": false, + "apply_defaults_to_fp_text": false, + "board_outline_line_width": 0.05, + "copper_line_width": 0.2, + "copper_text_italic": false, + "copper_text_size_h": 1.5, + "copper_text_size_v": 1.5, + "copper_text_thickness": 0.3, + "copper_text_upright": false, + "courtyard_line_width": 0.05, + "dimension_precision": 4, + "dimension_units": 3, + "dimensions": { + "arrow_length": 1270000, + "extension_offset": 500000, + "keep_text_aligned": true, + "suppress_zeroes": true, + "text_position": 0, + "units_format": 0 + }, + "fab_line_width": 0.1, + "fab_text_italic": false, + "fab_text_size_h": 1.0, + "fab_text_size_v": 1.0, + "fab_text_thickness": 0.15, + "fab_text_upright": false, + "other_line_width": 0.1, + "other_text_italic": false, + "other_text_size_h": 1.0, + "other_text_size_v": 1.0, + "other_text_thickness": 0.15, + "other_text_upright": false, + "pads": { + "drill": 1.5, + "height": 4.5, + "width": 2.5 + }, + "silk_line_width": 0.1, + "silk_text_italic": false, + "silk_text_size_h": 1.0, + "silk_text_size_v": 1.0, + "silk_text_thickness": 0.1, + "silk_text_upright": false, + "zones": { + "min_clearance": 0.5 + } + }, + "diff_pair_dimensions": [], + "drc_exclusions": [], + "meta": { + "version": 2 + }, + "rule_severities": { + "annular_width": "error", + "clearance": "error", + "connection_width": "warning", + "copper_edge_clearance": "error", + "copper_sliver": "warning", + "courtyards_overlap": "error", + "creepage": "error", + "diff_pair_gap_out_of_range": "error", + "diff_pair_uncoupled_length_too_long": "error", + "drill_out_of_range": "error", + "duplicate_footprints": "warning", + "extra_footprint": "warning", + "footprint": "error", + "footprint_filters_mismatch": "ignore", + "footprint_symbol_mismatch": "warning", + "footprint_type_mismatch": "ignore", + "hole_clearance": "error", + "hole_to_hole": "warning", + "holes_co_located": "warning", + "invalid_outline": "error", + "isolated_copper": "warning", + "item_on_disabled_layer": "error", + "items_not_allowed": "error", + "length_out_of_range": "error", + "lib_footprint_issues": "warning", + "lib_footprint_mismatch": "warning", + "malformed_courtyard": "error", + "microvia_drill_out_of_range": "error", + "mirrored_text_on_front_layer": "warning", + "missing_courtyard": "ignore", + "missing_footprint": "warning", + "net_conflict": "warning", + "nonmirrored_text_on_back_layer": "warning", + "npth_inside_courtyard": "ignore", + "padstack": "warning", + "pth_inside_courtyard": "ignore", + "shorting_items": "error", + "silk_edge_clearance": "warning", + "silk_over_copper": "warning", + "silk_overlap": "warning", + "skew_out_of_range": "error", + "solder_mask_bridge": "error", + "starved_thermal": "error", + "text_height": "warning", + "text_on_edge_cuts": "error", + "text_thickness": "warning", + "through_hole_pad_without_hole": "error", + "too_many_vias": "error", + "track_angle": "error", + "track_dangling": "warning", + "track_segment_length": "error", + "track_width": "error", + "tracks_crossing": "error", + "unconnected_items": "error", + "unresolved_variable": "error", + "via_dangling": "warning", + "zones_intersect": "error" + }, + "rules": { + "max_error": 0.005, + "min_clearance": 0.0, + "min_connection": 0.0, + "min_copper_edge_clearance": 0.5, + "min_groove_width": 0.0, + "min_hole_clearance": 0.25, + "min_hole_to_hole": 0.25, + "min_microvia_diameter": 0.2, + "min_microvia_drill": 0.1, + "min_resolved_spokes": 2, + "min_silk_clearance": 0.0, + "min_text_height": 0.8, + "min_text_thickness": 0.08, + "min_through_hole_diameter": 0.3, + "min_track_width": 0.0, + "min_via_annular_width": 0.1, + "min_via_diameter": 0.5, + "solder_mask_to_copper_clearance": 0.0, + "use_height_for_length_calcs": true + }, + "teardrop_options": [ + { + "td_onpthpad": true, + "td_onroundshapesonly": false, + "td_onsmdpad": true, + "td_ontrackend": false, + "td_onvia": true + } + ], + "teardrop_parameters": [ + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_round_shape", + "td_width_to_size_filter_ratio": 0.9 + }, + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_rect_shape", + "td_width_to_size_filter_ratio": 0.9 + }, + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_track_end", + "td_width_to_size_filter_ratio": 0.9 + } + ], + "track_widths": [], + "tuning_pattern_settings": { + "diff_pair_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 1.0 + }, + "diff_pair_skew_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 0.6 + }, + "single_track_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 0.6 + } + }, + "via_dimensions": [], + "zones_allow_external_fillets": false + }, + "ipc2581": { + "dist": "", + "distpn": "", + "internal_id": "", + "mfg": "", + "mpn": "" + }, + "layer_pairs": [], + "layer_presets": [], + "viewports": [] + }, + "boards": [], + "cvpcb": { + "equivalence_files": [] + }, + "erc": { + "erc_exclusions": [], + "meta": { + "version": 0 + }, + "pin_map": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 1, + 0, + 1, + 2 + ], + [ + 0, + 1, + 0, + 0, + 0, + 0, + 1, + 1, + 2, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 2 + ], + [ + 1, + 1, + 1, + 1, + 1, + 0, + 1, + 1, + 1, + 1, + 1, + 2 + ], + [ + 0, + 0, + 0, + 1, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 2, + 0, + 0, + 1, + 0, + 2, + 2, + 2, + 2 + ], + [ + 0, + 2, + 0, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 0, + 2, + 1, + 1, + 0, + 0, + 1, + 0, + 2, + 0, + 0, + 2 + ], + [ + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2, + 2 + ] + ], + "rule_severities": { + "bus_definition_conflict": "error", + "bus_entry_needed": "error", + "bus_to_bus_conflict": "error", + "bus_to_net_conflict": "error", + "different_unit_footprint": "error", + "different_unit_net": "error", + "duplicate_reference": "error", + "duplicate_sheet_names": "error", + "endpoint_off_grid": "warning", + "extra_units": "error", + "footprint_filter": "ignore", + "footprint_link_issues": "warning", + "four_way_junction": "ignore", + "global_label_dangling": "warning", + "hier_label_mismatch": "error", + "label_dangling": "error", + "label_multiple_wires": "warning", + "lib_symbol_issues": "warning", + "lib_symbol_mismatch": "warning", + "missing_bidi_pin": "warning", + "missing_input_pin": "warning", + "missing_power_pin": "error", + "missing_unit": "warning", + "multiple_net_names": "warning", + "net_not_bus_member": "warning", + "no_connect_connected": "warning", + "no_connect_dangling": "warning", + "pin_not_connected": "error", + "pin_not_driven": "error", + "pin_to_pin": "warning", + "power_pin_not_driven": "error", + "same_local_global_label": "warning", + "similar_label_and_power": "warning", + "similar_labels": "warning", + "similar_power": "warning", + "simulation_model_issue": "ignore", + "single_global_label": "ignore", + "unannotated": "error", + "unconnected_wire_endpoint": "warning", + "undefined_netclass": "error", + "unit_value_mismatch": "error", + "unresolved_variable": "error", + "wire_dangling": "error" + } + }, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "soldermask_test.kicad_pro", + "version": 3 + }, + "net_settings": { + "classes": [ + { + "bus_width": 12, + "clearance": 0.2, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.3, + "microvia_drill": 0.1, + "name": "Default", + "pcb_color": "rgba(0, 0, 0, 0.000)", + "priority": 2147483647, + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.2, + "via_diameter": 0.6, + "via_drill": 0.3, + "wire_width": 6 + } + ], + "meta": { + "version": 4 + }, + "net_colors": null, + "netclass_assignments": null, + "netclass_patterns": [] + }, + "pcbnew": { + "last_paths": { + "gencad": "", + "idf": "", + "netlist": "", + "plot": "", + "pos_files": "", + "specctra_dsn": "", + "step": "", + "svg": "", + "vrml": "" + }, + "page_layout_descr_file": "" + }, + "schematic": { + "annotate_start_num": 0, + "bom_export_filename": "${PROJECTNAME}.csv", + "bom_fmt_presets": [], + "bom_fmt_settings": { + "field_delimiter": ",", + "keep_line_breaks": false, + "keep_tabs": false, + "name": "CSV", + "ref_delimiter": ",", + "ref_range_delimiter": "", + "string_delimiter": "\"" + }, + "bom_presets": [], + "bom_settings": { + "exclude_dnp": false, + "fields_ordered": [ + { + "group_by": false, + "label": "Reference", + "name": "Reference", + "show": true + }, + { + "group_by": false, + "label": "Qty", + "name": "${QUANTITY}", + "show": true + }, + { + "group_by": true, + "label": "Value", + "name": "Value", + "show": true + }, + { + "group_by": true, + "label": "DNP", + "name": "${DNP}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from BOM", + "name": "${EXCLUDE_FROM_BOM}", + "show": true + }, + { + "group_by": true, + "label": "Exclude from Board", + "name": "${EXCLUDE_FROM_BOARD}", + "show": true + }, + { + "group_by": true, + "label": "Footprint", + "name": "Footprint", + "show": true + }, + { + "group_by": false, + "label": "Datasheet", + "name": "Datasheet", + "show": true + } + ], + "filter_string": "", + "group_symbols": true, + "include_excluded_from_bom": true, + "name": "Default Editing", + "sort_asc": true, + "sort_field": "Reference" + }, + "connection_grid_size": 50.0, + "drawing": { + "dashed_lines_dash_length_ratio": 12.0, + "dashed_lines_gap_length_ratio": 3.0, + "default_line_thickness": 6.0, + "default_text_size": 50.0, + "field_names": [], + "intersheets_ref_own_page": false, + "intersheets_ref_prefix": "", + "intersheets_ref_short": false, + "intersheets_ref_show": false, + "intersheets_ref_suffix": "", + "junction_size_choice": 3, + "label_size_ratio": 0.375, + "operating_point_overlay_i_precision": 3, + "operating_point_overlay_i_range": "~A", + "operating_point_overlay_v_precision": 3, + "operating_point_overlay_v_range": "~V", + "overbar_offset_ratio": 1.23, + "pin_symbol_size": 25.0, + "text_offset_ratio": 0.15 + }, + "legacy_lib_dir": "", + "legacy_lib_list": [], + "meta": { + "version": 1 + }, + "net_format_name": "", + "page_layout_descr_file": "", + "plot_directory": "", + "space_save_all_events": true, + "spice_current_sheet_as_root": false, + "spice_external_command": "spice \"%I\"", + "spice_model_current_sheet_as_root": true, + "spice_save_all_currents": false, + "spice_save_all_dissipations": false, + "spice_save_all_voltages": false, + "subpart_first_id": 65, + "subpart_id_separator": 0 + }, + "sheets": [], + "text_variables": {} +} diff --git a/qa/data/pcbnew/soldermask_track_to_pad.kicad_pcb b/qa/data/pcbnew/soldermask_track_to_pad.kicad_pcb new file mode 100644 index 0000000000..e7aed4a340 --- /dev/null +++ b/qa/data/pcbnew/soldermask_track_to_pad.kicad_pcb @@ -0,0 +1,131 @@ +(kicad_pcb + (version 20240108) + (generator "pcbnew") + (generator_version "8.0") + (general + (thickness 1.6) + (legacy_teardrops no) + ) + (paper "A4") + (layers + (0 "F.Cu" signal) + (31 "B.Cu" signal) + (32 "B.Adhes" user "B.Adhesive") + (33 "F.Adhes" user "F.Adhesive") + (34 "B.Paste" user) + (35 "F.Paste" user) + (36 "B.SilkS" user "B.Silkscreen") + (37 "F.SilkS" user "F.Silkscreen") + (38 "B.Mask" user) + (39 "F.Mask" user) + (40 "Dwgs.User" user "User.Drawings") + (41 "Cmts.User" user "User.Comments") + (42 "Eco1.User" user "User.Eco1") + (43 "Eco2.User" user "User.Eco2") + (44 "Edge.Cuts" user) + (45 "Margin" user) + (46 "B.CrtYd" user "B.Courtyard") + (47 "F.CrtYd" user "F.Courtyard") + (48 "B.Fab" user) + (49 "F.Fab" user) + (50 "User.1" user) + (51 "User.2" user) + (52 "User.3" user) + (53 "User.4" user) + (54 "User.5" user) + (55 "User.6" user) + (56 "User.7" user) + (57 "User.8" user) + (58 "User.9" user) + ) + (setup + (pad_to_mask_clearance 0.05) + (allow_soldermask_bridges_in_footprints no) + (pcbplotparams + (layerselection 0x00010fc_ffffffff) + (plot_on_all_layers_selection 0x0000000_00000000) + (disableapertmacros no) + (usegerberextensions no) + (usegerberattributes yes) + (usegerberadvancedattributes yes) + (creategerberjobfile yes) + (dashed_line_dash_ratio 12.000000) + (dashed_line_gap_ratio 3.000000) + (svgprecision 4) + (plotframeref no) + (viasonmask no) + (mode 1) + (useauxorigin no) + (hpglpennumber 1) + (hpglpenspeed 20) + (hpglpendiameter 15.000000) + (pdf_front_fp_property_popups yes) + (pdf_back_fp_property_popups yes) + (pdf_metadata yes) + (dxfpolygonmode yes) + (dxfimperialunits yes) + (dxfusepcbnewfont yes) + (psnegative no) + (psa4output no) + (plotreference yes) + (plotvalue yes) + (plotfptext yes) + (plotinvisibletext no) + (sketchpadsonfab no) + (subtractmaskfromsilk no) + (outputformat 1) + (mirror no) + (drillshape 1) + (scaleselection 1) + (outputdirectory "") + ) + ) + (net 0 "") + (net 1 "netA") + (net 2 "netB") + (footprint "TestPad" + (layer "B.Cu") + (uuid "11111111-1111-1111-1111-111111111111") + (at 100 100) + (property "Reference" "P1" + (at 0 -2 0) + (layer "B.SilkS") + (uuid "22222222-2222-2222-2222-222222222222") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + (justify mirror) + ) + ) + (property "Value" "TestPad" + (at 0 2 0) + (layer "B.Fab") + (uuid "33333333-3333-3333-3333-333333333333") + (effects + (font + (size 1 1) + (thickness 0.15) + ) + (justify mirror) + ) + ) + (pad "1" smd roundrect + (at 0 0) + (size 1 1) + (layers "B.Cu" "B.Paste" "B.Mask") + (roundrect_rratio 0.25) + (net 1 "netA") + (uuid "44444444-4444-4444-4444-444444444444") + ) + ) + (segment + (start 100.6 95) + (end 100.6 105) + (width 0.2) + (layer "B.Cu") + (net 2) + (uuid "55555555-5555-5555-5555-555555555555") + ) +) diff --git a/qa/data/pcbnew/soldermask_track_to_pad.kicad_pro b/qa/data/pcbnew/soldermask_track_to_pad.kicad_pro new file mode 100644 index 0000000000..bfac1edb3e --- /dev/null +++ b/qa/data/pcbnew/soldermask_track_to_pad.kicad_pro @@ -0,0 +1,284 @@ +{ + "board": { + "3dviewports": [], + "design_settings": { + "defaults": { + "apply_defaults_to_fp_fields": false, + "apply_defaults_to_fp_shapes": false, + "apply_defaults_to_fp_text": false, + "board_outline_line_width": 0.1, + "copper_line_width": 0.2, + "copper_text_italic": false, + "copper_text_size_h": 1.5, + "copper_text_size_v": 1.5, + "copper_text_thickness": 0.3, + "copper_text_upright": false, + "courtyard_line_width": 0.05, + "dimension_precision": 4, + "dimension_units": 3, + "dimensions": { + "arrow_length": 1270000, + "extension_offset": 500000, + "keep_text_aligned": true, + "suppress_zeroes": true, + "text_position": 0, + "units_format": 0 + }, + "fab_line_width": 0.1, + "fab_text_italic": false, + "fab_text_size_h": 1.0, + "fab_text_size_v": 1.0, + "fab_text_thickness": 0.15, + "fab_text_upright": false, + "other_line_width": 0.1, + "other_text_italic": false, + "other_text_size_h": 1.0, + "other_text_size_v": 1.0, + "other_text_thickness": 0.15, + "other_text_upright": false, + "pads": { + "drill": 0.8, + "height": 1.27, + "width": 2.54 + }, + "silk_line_width": 0.1, + "silk_text_italic": false, + "silk_text_size_h": 1.0, + "silk_text_size_v": 1.0, + "silk_text_thickness": 0.1, + "silk_text_upright": false, + "zones": { + "min_clearance": 0.5 + } + }, + "diff_pair_dimensions": [], + "drc_exclusions": [], + "meta": { + "version": 2 + }, + "rule_severities": { + "annular_width": "error", + "clearance": "error", + "connection_width": "warning", + "copper_edge_clearance": "error", + "copper_sliver": "warning", + "courtyards_overlap": "error", + "creepage": "error", + "diff_pair_gap_out_of_range": "error", + "diff_pair_uncoupled_length_too_long": "error", + "drill_out_of_range": "error", + "duplicate_footprints": "warning", + "extra_footprint": "warning", + "footprint": "error", + "footprint_filters_mismatch": "ignore", + "footprint_symbol_mismatch": "warning", + "footprint_type_mismatch": "ignore", + "hole_clearance": "error", + "hole_to_hole": "warning", + "holes_co_located": "warning", + "invalid_outline": "error", + "isolated_copper": "warning", + "item_on_disabled_layer": "error", + "items_not_allowed": "error", + "length_out_of_range": "error", + "lib_footprint_issues": "warning", + "lib_footprint_mismatch": "warning", + "malformed_courtyard": "error", + "microvia_drill_out_of_range": "error", + "mirrored_text_on_front_layer": "warning", + "missing_courtyard": "ignore", + "missing_footprint": "warning", + "net_conflict": "warning", + "nonmirrored_text_on_back_layer": "warning", + "npth_inside_courtyard": "ignore", + "padstack": "warning", + "pth_inside_courtyard": "ignore", + "shorting_items": "error", + "silk_edge_clearance": "warning", + "silk_over_copper": "warning", + "silk_overlap": "warning", + "skew_out_of_range": "error", + "solder_mask_bridge": "error", + "starved_thermal": "error", + "text_height": "warning", + "text_on_edge_cuts": "error", + "text_thickness": "warning", + "through_hole_pad_without_hole": "error", + "too_many_vias": "error", + "track_angle": "error", + "track_dangling": "warning", + "track_segment_length": "error", + "track_width": "error", + "tracks_crossing": "error", + "unconnected_items": "error", + "unresolved_variable": "error", + "via_dangling": "warning", + "zones_intersect": "error" + }, + "rules": { + "max_error": 0.005, + "min_clearance": 0.0, + "min_connection": 0.0, + "min_copper_edge_clearance": 0.5, + "min_groove_width": 0.0, + "min_hole_clearance": 0.25, + "min_hole_to_hole": 0.25, + "min_microvia_diameter": 0.2, + "min_microvia_drill": 0.1, + "min_resolved_spokes": 2, + "min_silk_clearance": 0.0, + "min_text_height": 0.8, + "min_text_thickness": 0.08, + "min_through_hole_diameter": 0.3, + "min_track_width": 0.0, + "min_via_annular_width": 0.1, + "min_via_diameter": 0.5, + "solder_mask_to_copper_clearance": 1.0, + "use_height_for_length_calcs": true + }, + "teardrop_options": [ + { + "td_onpthpad": true, + "td_onroundshapesonly": false, + "td_onsmdpad": true, + "td_ontrackend": false, + "td_onvia": true + } + ], + "teardrop_parameters": [ + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_round_shape", + "td_width_to_size_filter_ratio": 0.9 + }, + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_rect_shape", + "td_width_to_size_filter_ratio": 0.9 + }, + { + "td_allow_use_two_tracks": true, + "td_curve_segcount": 0, + "td_height_ratio": 1.0, + "td_length_ratio": 0.5, + "td_maxheight": 2.0, + "td_maxlen": 1.0, + "td_on_pad_in_zone": false, + "td_target_name": "td_track_end", + "td_width_to_size_filter_ratio": 0.9 + } + ], + "track_widths": [], + "tuning_pattern_settings": { + "diff_pair_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 1.0 + }, + "diff_pair_skew_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 0.6 + }, + "single_track_defaults": { + "corner_radius_percentage": 80, + "corner_style": 1, + "max_amplitude": 1.0, + "min_amplitude": 0.2, + "single_sided": false, + "spacing": 0.6 + } + }, + "via_dimensions": [], + "zones_allow_external_fillets": false + }, + "ipc2581": { + "dist": "", + "distpn": "", + "internal_id": "", + "mfg": "", + "mpn": "" + }, + "layer_pairs": [], + "layer_presets": [], + "viewports": [] + }, + "boards": [], + "cvpcb": { + "equivalence_files": [] + }, + "libraries": { + "pinned_footprint_libs": [], + "pinned_symbol_libs": [] + }, + "meta": { + "filename": "soldermask_track_to_pad.kicad_pro", + "version": 3 + }, + "net_settings": { + "classes": [ + { + "bus_width": 12, + "clearance": 0.2, + "diff_pair_gap": 0.25, + "diff_pair_via_gap": 0.25, + "diff_pair_width": 0.2, + "line_style": 0, + "microvia_diameter": 0.3, + "microvia_drill": 0.1, + "name": "Default", + "pcb_color": "rgba(0, 0, 0, 0.000)", + "priority": 2147483647, + "schematic_color": "rgba(0, 0, 0, 0.000)", + "track_width": 0.2, + "via_diameter": 0.6, + "via_drill": 0.3, + "wire_width": 6 + } + ], + "meta": { + "version": 4 + }, + "net_colors": null, + "netclass_assignments": null, + "netclass_patterns": [] + }, + "pcbnew": { + "last_paths": { + "gencad": "", + "idf": "", + "netlist": "", + "plot": "", + "pos_files": "", + "specctra_dsn": "", + "step": "", + "svg": "", + "vrml": "" + }, + "page_layout_descr_file": "" + }, + "schematic": { + "legacy_lib_dir": "", + "legacy_lib_list": [] + }, + "sheets": [], + "text_variables": {} +} diff --git a/qa/tests/pcbnew/CMakeLists.txt b/qa/tests/pcbnew/CMakeLists.txt index 89b2a726b3..b2c07db8ba 100644 --- a/qa/tests/pcbnew/CMakeLists.txt +++ b/qa/tests/pcbnew/CMakeLists.txt @@ -77,6 +77,8 @@ set( QA_PCBNEW_SRCS drc/test_drc_copper_graphics.cpp drc/test_drc_copper_sliver.cpp drc/test_solder_mask_bridging.cpp + drc/test_solder_mask_bridge_all_tracks.cpp + drc/test_solder_mask_track_to_pad.cpp drc/test_drc_multi_netclasses.cpp drc/test_drc_skew.cpp drc/test_drc_component_classes.cpp diff --git a/qa/tests/pcbnew/drc/test_solder_mask_bridge_all_tracks.cpp b/qa/tests/pcbnew/drc/test_solder_mask_bridge_all_tracks.cpp new file mode 100644 index 0000000000..e595a38a9c --- /dev/null +++ b/qa/tests/pcbnew/drc/test_solder_mask_bridge_all_tracks.cpp @@ -0,0 +1,192 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * @file test_solder_mask_bridge_all_tracks.cpp + * Test that soldermask bridging reports all track combinations when "report all track errors" + * is enabled. + * + * Bug description: When DRC checks for soldermask bridging between two nets, only one object + * from net A is reported against all objects from net B. With "report all track errors" enabled, + * all track combinations should be reported. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + + +struct DRC_SOLDER_MASK_BRIDGE_ALL_TRACKS_FIXTURE +{ + DRC_SOLDER_MASK_BRIDGE_ALL_TRACKS_FIXTURE() + { } + + SETTINGS_MANAGER m_settingsManager; + std::unique_ptr m_board; +}; + + +BOOST_FIXTURE_TEST_CASE( DRCSolderMaskBridgeAllTracksTest, DRC_SOLDER_MASK_BRIDGE_ALL_TRACKS_FIXTURE ) +{ + // Test board has: + // - Net "a" with 4 track segments + // - Net "b" with 4 track segments + // - A soldermask opening (gr_rect on F.Mask) covering where tracks from both nets pass through + // + // With "report all track errors" enabled, we expect all combinations of net A tracks vs net B + // tracks to be reported. Each track from net A that bridges with each track from net B should + // generate a violation. + + wxString brd_name( wxT( "soldermask_bridge_all_tracks" ) ); + KI_TEST::LoadBoard( m_settingsManager, brd_name, m_board ); + + std::vector violations; + BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings(); + + // Disable DRC tests not relevant to this test + bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_ISSUES ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_MISMATCH ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_COPPER_SLIVER ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_STARVED_THERMAL ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_SILK_CLEARANCE ] = SEVERITY::RPT_SEVERITY_IGNORE; + + bds.m_DRCEngine->SetViolationHandler( + [&]( const std::shared_ptr& aItem, const VECTOR2I& aPos, int aLayer, + const std::function& aPathGenerator ) + { + if( aItem->GetErrorCode() == DRCE_SOLDERMASK_BRIDGE ) + violations.push_back( *aItem ); + } ); + + // Count tracks per net to understand expected violations + int netA_tracks = 0; + int netB_tracks = 0; + + for( PCB_TRACK* track : m_board->Tracks() ) + { + if( track->GetNetname() == wxT( "a" ) ) + netA_tracks++; + else if( track->GetNetname() == wxT( "b" ) ) + netB_tracks++; + } + + BOOST_TEST_MESSAGE( wxString::Format( "Net A has %d tracks, Net B has %d tracks", + netA_tracks, netB_tracks ) ); + + // Run DRC with "report all track errors" = true + bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true /* aReportAllTrackErrors */, false ); + + // With "report all track errors" enabled, we expect all track pair combinations to be reported. + // In the test board, both nets have 4 track segments each that pass through the soldermask + // opening. The soldermask aperture (gr_rect) bridges copper from different nets. + // + // Each track from net A that is close enough to each track from net B within the soldermask + // opening should generate a violation. For this test case, we expect multiple violations, + // not just one. + // + // The exact number depends on which track segments are actually within clearance distance + // in the soldermask opening area. + + // We expect MORE violations when reporting all track errors than in single error mode. + // With 4 tracks + 1 pad on each net passing through the soldermask opening: + // - Non-track items (pads) always get all combinations reported + // - Track items get all combinations only when "report all track errors" is enabled + // + // The original bug was that all violations showed the same track from net A (the first + // one cached), regardless of how many tracks actually violated. Now: + // - All non-track items from net A are reported against each net B collision + // - All track items from net A are reported (when option is set) against each net B collision + BOOST_CHECK_GT( violations.size(), 5 ); + + BOOST_TEST_MESSAGE( wxString::Format( "Found %zu soldermask bridge violations", + violations.size() ) ); + + // Verify that we're reporting different items from net A (not just the first cached one) + std::set netAItemIds; + + for( const DRC_ITEM& item : violations ) + { + if( item.GetAuxItemID() != niluuid ) + netAItemIds.insert( item.GetAuxItemID() ); + } + + // With 4 tracks + 1 pad on net A, we should see multiple different items reported + BOOST_CHECK_GE( netAItemIds.size(), 4 ); + + BOOST_TEST_MESSAGE( wxString::Format( "Found %zu different net A items in violations", + netAItemIds.size() ) ); +} + + +BOOST_FIXTURE_TEST_CASE( DRCSolderMaskBridgeSingleErrorTest, DRC_SOLDER_MASK_BRIDGE_ALL_TRACKS_FIXTURE ) +{ + // When "report all track errors" is disabled, we should get fewer violations + // (the original behavior - only report one error per track connection) + + wxString brd_name( wxT( "soldermask_bridge_all_tracks" ) ); + KI_TEST::LoadBoard( m_settingsManager, brd_name, m_board ); + + std::vector violations; + BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings(); + + bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_ISSUES ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_MISMATCH ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_COPPER_SLIVER ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_STARVED_THERMAL ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_SILK_CLEARANCE ] = SEVERITY::RPT_SEVERITY_IGNORE; + + bds.m_DRCEngine->SetViolationHandler( + [&]( const std::shared_ptr& aItem, const VECTOR2I& aPos, int aLayer, + const std::function& aPathGenerator ) + { + if( aItem->GetErrorCode() == DRCE_SOLDERMASK_BRIDGE ) + violations.push_back( *aItem ); + } ); + + // Run DRC with "report all track errors" = false + bds.m_DRCEngine->RunTests( EDA_UNITS::MM, false /* aReportAllTrackErrors */, false ); + + size_t singleErrorViolations = violations.size(); + + BOOST_TEST_MESSAGE( wxString::Format( "Single error mode: Found %zu soldermask bridge violations", + singleErrorViolations ) ); + + // Run again with "report all track errors" = true for comparison + violations.clear(); + bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true /* aReportAllTrackErrors */, false ); + + size_t allErrorsViolations = violations.size(); + + BOOST_TEST_MESSAGE( wxString::Format( "All errors mode: Found %zu soldermask bridge violations", + allErrorsViolations ) ); + + // When reporting all errors, we should have at least as many (likely more) violations + BOOST_CHECK_GE( allErrorsViolations, singleErrorViolations ); +} diff --git a/qa/tests/pcbnew/drc/test_solder_mask_track_to_pad.cpp b/qa/tests/pcbnew/drc/test_solder_mask_track_to_pad.cpp new file mode 100644 index 0000000000..09d9949baf --- /dev/null +++ b/qa/tests/pcbnew/drc/test_solder_mask_track_to_pad.cpp @@ -0,0 +1,142 @@ +/* + * This program source code file is part of KiCad, a free EDA CAD application. + * + * Copyright The KiCad Developers, see AUTHORS.txt for contributors. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, you may find one here: + * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html + * or you may search the http://www.gnu.org website for the version 2 license, + * or you may write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * @file test_solder_mask_track_to_pad.cpp + * Test soldermask bridging detection between tracks and pads from different nets. + * + * Bug description: When a track passes near a pad from a different net, and that track + * is within the pad's soldermask aperture + SolderMaskToCopperClearance, a bridging error + * should be reported. This was not happening because the R-tree query clearance did not + * include SolderMaskToCopperClearance. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + + +struct DRC_TRACK_TO_PAD_BRIDGE_FIXTURE +{ + DRC_TRACK_TO_PAD_BRIDGE_FIXTURE() + { } + + SETTINGS_MANAGER m_settingsManager; + std::unique_ptr m_board; +}; + + +BOOST_FIXTURE_TEST_CASE( DRCTrackToPadBridgeTest, DRC_TRACK_TO_PAD_BRIDGE_FIXTURE ) +{ + // Test board has: + // - A pad on B.Cu with B.Mask at position (100, 100) on net "netA" + // - A track on B.Cu at x=100.6 (0.6mm from pad center) on net "netB" + // - SolderMaskToCopperClearance set to 1.0mm + // + // The track is 0.6mm from the pad center. With a 1mm pad and 0.05mm mask expansion, + // the pad edge is at 100.5mm and mask aperture edge is at 100.55mm. + // The track (0.2mm wide) has its nearest edge at 100.5mm. + // + // The track copper is within the SolderMaskToCopperClearance of the pad's mask aperture, + // so a violation should be reported. + + wxString brd_name( wxT( "soldermask_track_to_pad" ) ); + KI_TEST::LoadBoard( m_settingsManager, brd_name, m_board ); + + std::vector violations; + BOARD_DESIGN_SETTINGS& bds = m_board->GetDesignSettings(); + + // Verify the clearance is set + BOOST_TEST_MESSAGE( wxString::Format( "SolderMaskToCopperClearance: %d nm", + bds.m_SolderMaskToCopperClearance ) ); + + // Disable DRC tests not relevant to this test + bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_ISSUES ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_LIB_FOOTPRINT_MISMATCH ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_COPPER_SLIVER ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_STARVED_THERMAL ] = SEVERITY::RPT_SEVERITY_IGNORE; + bds.m_DRCSeverities[ DRCE_SILK_CLEARANCE ] = SEVERITY::RPT_SEVERITY_IGNORE; + + bds.m_DRCEngine->SetViolationHandler( + [&]( const std::shared_ptr& aItem, const VECTOR2I& aPos, int aLayer, + const std::function& aPathGenerator ) + { + if( aItem->GetErrorCode() == DRCE_SOLDERMASK_BRIDGE ) + violations.push_back( *aItem ); + } ); + + // Run DRC + bds.m_DRCEngine->RunTests( EDA_UNITS::MM, true /* aReportAllTrackErrors */, false ); + + BOOST_TEST_MESSAGE( wxString::Format( "Found %zu soldermask bridge violations", + violations.size() ) ); + + // We expect at least one violation: track from netB is within clearance of pad from netA + BOOST_CHECK_GE( violations.size(), 1 ); + + // Verify the violation involves the track by checking item IDs in the cache + bool foundTrackViolation = false; + const auto& itemCache = m_board->GetItemByIdCache(); + + for( const DRC_ITEM& item : violations ) + { + KIID mainId = item.GetMainItemID(); + KIID auxId = item.GetAuxItemID(); + + BOARD_ITEM* mainItem = nullptr; + BOARD_ITEM* auxItem = nullptr; + + if( mainId != niluuid ) + { + auto it = itemCache.find( mainId ); + + if( it != itemCache.end() ) + mainItem = it->second; + } + + if( auxId != niluuid ) + { + auto it = itemCache.find( auxId ); + + if( it != itemCache.end() ) + auxItem = it->second; + } + + BOOST_TEST_MESSAGE( wxString::Format( "Violation: main=%s aux=%s", + mainItem ? mainItem->GetClass() : "null", + auxItem ? auxItem->GetClass() : "null" ) ); + + if( ( mainItem && mainItem->Type() == PCB_TRACE_T ) || + ( auxItem && auxItem->Type() == PCB_TRACE_T ) ) + { + foundTrackViolation = true; + } + } + + BOOST_CHECK_MESSAGE( foundTrackViolation, "Expected to find a track-to-pad soldermask violation" ); +}