KiCad PCB EDA Suite
Loading...
Searching...
No Matches
drc_re_rule_saver.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include "drc_re_rule_saver.h"
25
26#include <board.h>
27#include <lset.h>
28#include <string_utils.h>
29#include <wx/ffile.h>
30
31#include <drc_rules_lexer.h>
32
35
36
37namespace
38{
39
40wxString formatLayerClause( const wxString& aLayerSource )
41{
42 if( aLayerSource.IsEmpty() )
43 return wxEmptyString;
44
45 if( aLayerSource == DRC_RULES_LEXER::TokenName( DRCRULE_T::T_outer )
46 || aLayerSource == DRC_RULES_LEXER::TokenName( DRCRULE_T::T_inner ) )
47 return wxString::Format( wxS( "(layer %s)" ), aLayerSource );
48
49 return wxString::Format( wxS( "(layer \"%s\")" ), EscapeString( aLayerSource, CTX_QUOTED_STR ) );
50}
51
52}
53
54
58
59
60bool DRC_RULE_SAVER::SaveFile( const wxString& aPath,
61 const std::vector<DRC_RE_LOADED_PANEL_ENTRY>& aEntries,
62 const BOARD* aBoard )
63{
64 wxFFile file( aPath, "w" );
65
66 if( !file.IsOpened() )
67 return false;
68
69 wxString content = GenerateRulesText( aEntries, aBoard );
70 file.Write( content );
71 file.Close();
72
73 return true;
74}
75
76
77wxString DRC_RULE_SAVER::GenerateRulesText( const std::vector<DRC_RE_LOADED_PANEL_ENTRY>& aEntries,
78 const BOARD* aBoard )
79{
80 wxString result = "(version 1)\n";
81
82 // Group entries by (ruleName, condition, layerSource) for merging same-rule constraints.
83 // Including the layer source prevents rules with different layer scopes from being
84 // incorrectly merged (e.g. separate "outer" and "inner" rules must remain distinct).
85 using GroupKey = std::tuple<wxString, wxString, wxString>;
86
87 std::vector<std::pair<GroupKey, std::vector<const DRC_RE_LOADED_PANEL_ENTRY*>>>
88 groupedEntries;
89 std::map<GroupKey, size_t> groupIndex;
90
91 for( const DRC_RE_LOADED_PANEL_ENTRY& entry : aEntries )
92 {
93 auto key = std::make_tuple( entry.ruleName, entry.condition, entry.layerSource );
94 auto it = groupIndex.find( key );
95
96 if( it == groupIndex.end() )
97 {
98 groupIndex[key] = groupedEntries.size();
99 groupedEntries.push_back( { key, { &entry } } );
100 }
101 else
102 {
103 groupedEntries[it->second].second.push_back( &entry );
104 }
105 }
106
107 // Generate rule text for each group
108 for( const auto& [key, entries] : groupedEntries )
109 {
110 wxString ruleText;
111
112 if( entries.size() == 1 )
113 {
114 // Single entry, no merge needed
115 ruleText = generateRuleText( *entries[0], aBoard );
116 }
117 else
118 {
119 // Multiple entries with same name/condition need merging
120 ruleText = generateMergedRuleText( entries, aBoard );
121 }
122
123 if( !ruleText.IsEmpty() )
124 result += ruleText + "\n";
125 }
126
127 return result;
128}
129
130
132 const BOARD* aBoard )
133{
134 // Round-trip preservation: return original text if not edited
135 if( !aEntry.wasEdited && !aEntry.originalRuleText.IsEmpty() )
136 return aEntry.originalRuleText;
137
138 // Otherwise, regenerate from panel data
139 if( !aEntry.constraintData )
140 return wxEmptyString;
141
142 wxString ruleText = aEntry.constraintData->GetGeneratedRule();
143
144 if( ruleText.IsEmpty() || aEntry.panelType == SILK_TO_SOLDERMASK_CLEARANCE
145 || aEntry.panelType == SILK_TO_SILK_CLEARANCE )
146 {
148 ctx.ruleName = aEntry.ruleName;
149 ctx.conditionExpression = aEntry.condition;
151 ctx.comment = aEntry.constraintData->GetComment();
152
154 {
155 wxString silkCond;
156
157 if( aEntry.layerCondition.test( F_SilkS ) && !aEntry.layerCondition.test( B_SilkS ) )
158 silkCond = wxS( "L == 'F.Mask'" );
159 else if( aEntry.layerCondition.test( B_SilkS ) && !aEntry.layerCondition.test( F_SilkS ) )
160 silkCond = wxS( "L == 'B.Mask'" );
161 else
162 silkCond = wxS( "L == 'F.Mask' || L == 'B.Mask'" );
163
164 if( !ctx.conditionExpression.IsEmpty() )
165 ctx.conditionExpression = wxS( "(" ) + silkCond + wxS( ") && " ) + ctx.conditionExpression;
166 else
167 ctx.conditionExpression = silkCond;
168 }
169 else if( aEntry.panelType == SILK_TO_SILK_CLEARANCE )
170 {
171 wxString silkCond;
172
173 if( aEntry.layerCondition.test( F_SilkS ) && !aEntry.layerCondition.test( B_SilkS ) )
174 silkCond = wxS( "L == 'F.SilkS'" );
175 else if( aEntry.layerCondition.test( B_SilkS ) && !aEntry.layerCondition.test( F_SilkS ) )
176 silkCond = wxS( "L == 'B.SilkS'" );
177 else
178 silkCond = wxS( "L == 'F.SilkS' || L == 'B.SilkS'" );
179
180 if( !ctx.conditionExpression.IsEmpty() )
181 ctx.conditionExpression = wxS( "(" ) + silkCond + wxS( ") && " ) + ctx.conditionExpression;
182 else
183 ctx.conditionExpression = silkCond;
184 }
185 else if( aBoard )
186 {
187 if( !aEntry.layerSource.IsEmpty() )
188 ctx.layerClause = formatLayerClause( aEntry.layerSource );
189 else
190 ctx.layerClause = generateLayerClause( aEntry.layerCondition, aBoard );
191 }
192
193 ruleText = aEntry.constraintData->GenerateRule( ctx );
194 }
195
196 // If severity is specified and not default, we need to inject it
197 // The GenerateRule method should handle this, but we verify here
199 {
200 wxString severityClause = generateSeverityClause( aEntry.severity );
201
202 if( !severityClause.IsEmpty() && !ruleText.Contains( "(severity" ) )
203 {
204 // Insert severity clause before the closing paren
205 size_t lastParen = ruleText.rfind( ')' );
206
207 if( lastParen != wxString::npos )
208 {
209 ruleText = ruleText.Left( lastParen ) + "\n\t" + severityClause + ")";
210 }
211 }
212 }
213
214 return ruleText;
215}
216
217
218wxString DRC_RULE_SAVER::generateLayerClause( const LSET& aLayers, const BOARD* aBoard )
219{
220 if( !aBoard || !aLayers.any() )
221 return wxEmptyString;
222
223 if( ( aLayers & LSET::AllCuMask() ) == LSET::ExternalCuMask() )
224 return wxString::Format( wxS( "(layer %s)" ), DRC_RULES_LEXER::TokenName( DRCRULE_T::T_outer ) );
225
226 if( ( aLayers & LSET::AllCuMask() ) == LSET::InternalCuMask() )
227 return wxString::Format( wxS( "(layer %s)" ), DRC_RULES_LEXER::TokenName( DRCRULE_T::T_inner ) );
228
229 // The parser only accepts a single layer name, so emit the first matching layer.
230 // Multi-layer conditions should use "outer" or "inner" keywords above.
231 for( PCB_LAYER_ID layer : aLayers.Seq() )
232 return wxString::Format( wxS( "(layer \"%s\")" ), aBoard->GetLayerName( layer ) );
233
234 return wxEmptyString;
235}
236
237
239{
240 switch( aSeverity )
241 {
242 case RPT_SEVERITY_IGNORE: return "(severity ignore)";
243 case RPT_SEVERITY_WARNING: return "(severity warning)";
244 case RPT_SEVERITY_ERROR: return "(severity error)";
245 case RPT_SEVERITY_EXCLUSION: return "(severity exclusion)";
246 default: return wxEmptyString;
247 }
248}
249
250
252 const std::vector<const DRC_RE_LOADED_PANEL_ENTRY*>& aEntries,
253 const BOARD* aBoard )
254{
255 if( aEntries.empty() )
256 return wxEmptyString;
257
258 // Check if all entries are unedited and the first one has original text
259 // If so, we can use round-trip preservation
260 bool allUnedited = true;
261
262 for( const auto* entry : aEntries )
263 {
264 if( entry->wasEdited )
265 {
266 allUnedited = false;
267 break;
268 }
269 }
270
271 if( allUnedited && !aEntries[0]->originalRuleText.IsEmpty() )
272 return aEntries[0]->originalRuleText;
273
274 // Otherwise, merge constraint clauses from all entries
275 const DRC_RE_LOADED_PANEL_ENTRY* firstEntry = aEntries[0];
276
278 ctx.ruleName = firstEntry->ruleName;
279 ctx.conditionExpression = firstEntry->condition;
280
281 // Generate layer clause from first entry with layer info (all entries in a
282 // merged group share the same layerSource because it's part of the grouping key)
283 for( const auto* entry : aEntries )
284 {
285 if( entry->layerCondition.any() && aBoard )
286 {
287 if( !entry->layerSource.IsEmpty() )
288 ctx.layerClause = formatLayerClause( entry->layerSource );
289 else
290 ctx.layerClause = generateLayerClause( entry->layerCondition, aBoard );
291
292 break;
293 }
294 }
295
296 // Collect all constraint clauses from all entries
297 std::vector<wxString> allClauses;
298
299 for( const auto* entry : aEntries )
300 {
301 if( entry->constraintData )
302 {
304 entryCtx.ruleName = entry->ruleName;
305 entryCtx.conditionExpression = entry->condition;
306 entryCtx.constraintCode = entry->constraintData->GetConstraintCode();
307
308 auto clauses = entry->constraintData->GetConstraintClauses( entryCtx );
309
310 for( const wxString& clause : clauses )
311 {
312 if( !clause.IsEmpty() )
313 allClauses.push_back( clause );
314 }
315 }
316 }
317
318 // Build the merged rule
319 wxString rule;
320 rule << wxS( "(rule " ) << DRC_RE_BASE_CONSTRAINT_DATA::sanitizeRuleName( ctx.ruleName )
321 << wxS( "\n" );
322
323 if( !ctx.layerClause.IsEmpty() )
324 rule << wxS( "\t" ) << ctx.layerClause << wxS( "\n" );
325
326 for( const wxString& clause : allClauses )
327 rule << wxS( "\t" ) << clause << wxS( "\n" );
328
329 if( !ctx.conditionExpression.IsEmpty() )
330 {
331 rule << wxS( "\t(condition \"" )
332 << EscapeString( ctx.conditionExpression, CTX_QUOTED_STR ) << wxS( "\")\n" );
333 }
334
335 // Add severity if any entry has non-default severity
336 for( const auto* entry : aEntries )
337 {
338 if( entry->severity != RPT_SEVERITY_UNDEFINED && entry->severity != RPT_SEVERITY_ERROR )
339 {
340 rule << wxS( "\t" ) << generateSeverityClause( entry->severity ) << wxS( "\n" );
341 break;
342 }
343 }
344
345 rule << wxS( ")" );
346
347 return rule;
348}
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:729
static wxString sanitizeRuleName(const wxString &aRuleName)
Sanitize a rule name for use in S-expression output.
virtual wxString GenerateRule(const RULE_GENERATION_CONTEXT &aContext)
bool SaveFile(const wxString &aPath, const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr)
Save all panel entries to a file.
wxString generateLayerClause(const LSET &aLayers, const BOARD *aBoard)
Generate a layer clause from an LSET.
wxString generateRuleText(const DRC_RE_LOADED_PANEL_ENTRY &aEntry, const BOARD *aBoard)
Generate the rule text for a single panel entry.
wxString GenerateRulesText(const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr)
Generate rule text from panel entries.
wxString generateMergedRuleText(const std::vector< const DRC_RE_LOADED_PANEL_ENTRY * > &aEntries, const BOARD *aBoard)
Generate a merged rule text from multiple panel entries with the same name/condition.
wxString generateSeverityClause(SEVERITY aSeverity)
Generate a severity clause.
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static const LSET & ExternalCuMask()
Return a mask holding the Front and Bottom layers.
Definition lset.cpp:634
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition lset.cpp:313
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:599
static const LSET & InternalCuMask()
Return a complete set of internal copper layers which is all Cu layers except F_Cu and B_Cu.
Definition lset.cpp:577
wxString GetComment()
Get the comment associated with the rule.
@ SILK_TO_SILK_CLEARANCE
@ SILK_TO_SOLDERMASK_CLEARANCE
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ F_SilkS
Definition layer_ids.h:100
@ B_SilkS
Definition layer_ids.h:101
SEVERITY
@ RPT_SEVERITY_WARNING
@ RPT_SEVERITY_ERROR
@ RPT_SEVERITY_UNDEFINED
@ RPT_SEVERITY_EXCLUSION
@ RPT_SEVERITY_IGNORE
wxString EscapeString(const wxString &aSource, ESCAPE_CONTEXT aContext)
The Escape/Unescape routines use HTML-entity-reference-style encoding to handle characters which are:...
@ CTX_QUOTED_STR
Represents a rule loaded from a .kicad_dru file and mapped to a panel.
wxString ruleName
wxString originalRuleText
wxString condition
bool wasEdited
wxString layerSource
Original layer text: "inner", "outer", or layer name.
LSET layerCondition
std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > constraintData
DRC_RULE_EDITOR_CONSTRAINT_NAME panelType
SEVERITY severity
wxString result
Test unit parsing edge cases and error handling.