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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include "drc_re_rule_saver.h"
21
22#include <board.h>
23#include <kiplatform/io.h>
24#include <lset.h>
25#include <string_utils.h>
26
27#include <drc_rules_lexer.h>
28
31
32
33namespace
34{
35
36wxString formatLayerClause( const wxString& aLayerSource )
37{
38 if( aLayerSource.IsEmpty() )
39 return wxEmptyString;
40
41 if( aLayerSource == DRC_RULES_LEXER::TokenName( DRCRULE_T::T_outer )
42 || aLayerSource == DRC_RULES_LEXER::TokenName( DRCRULE_T::T_inner ) )
43 return wxString::Format( wxS( "(layer %s)" ), aLayerSource );
44
45 return wxString::Format( wxS( "(layer \"%s\")" ), EscapeString( aLayerSource, CTX_QUOTED_STR ) );
46}
47
48}
49
50
54
55
56bool DRC_RULE_SAVER::SaveFile( const wxString& aPath,
57 const std::vector<DRC_RE_LOADED_PANEL_ENTRY>& aEntries,
58 const BOARD* aBoard,
59 const DRC_RE_FILE_TRIVIA& aTrivia )
60{
61 wxString content = GenerateRulesText( aEntries, aBoard, aTrivia );
62 std::string utf8 = std::string( content.mb_str( wxConvUTF8 ) );
63
64 return KIPLATFORM::IO::AtomicWriteFile( aPath, utf8.data(), utf8.size() );
65}
66
67
68wxString DRC_RULE_SAVER::GenerateRulesText( const std::vector<DRC_RE_LOADED_PANEL_ENTRY>& aEntries,
69 const BOARD* aBoard,
70 const DRC_RE_FILE_TRIVIA& aTrivia )
71{
72 wxString result = aTrivia.header;
73
74 if( result.IsEmpty() )
75 result = wxS( "(version 2)" );
76
77 // Group entries by (sourceRule, ruleName, condition, layerSource) for merging same-rule constraints.
78 // Including the layer source prevents rules with different layer scopes from being
79 // incorrectly merged (e.g. separate "outer" and "inner" rules must remain distinct).
80 using GroupKey = std::tuple<int, wxString, wxString, wxString>;
81
82 std::vector<std::pair<GroupKey, std::vector<const DRC_RE_LOADED_PANEL_ENTRY*>>>
83 groupedEntries;
84 std::map<GroupKey, size_t> groupIndex;
85
86 for( const DRC_RE_LOADED_PANEL_ENTRY& entry : aEntries )
87 {
88 auto key = std::make_tuple( entry.sourceRule, entry.ruleName, entry.condition, entry.layerSource );
89 auto it = groupIndex.find( key );
90
91 if( it == groupIndex.end() )
92 {
93 groupIndex[key] = groupedEntries.size();
94 groupedEntries.push_back( { key, { &entry } } );
95 }
96 else
97 {
98 groupedEntries[it->second].second.push_back( &entry );
99 }
100 }
101
102 size_t nextTrivia = 0;
103
104 // Generate rule text for each group
105 for( const auto& [key, entries] : groupedEntries )
106 {
107 wxString ruleText;
108
109 if( entries.size() == 1 )
110 {
111 // Single entry, no merge needed
112 ruleText = generateRuleText( *entries[0], aBoard );
113 }
114 else
115 {
116 // Multiple entries with same name/condition need merging
117 ruleText = generateMergedRuleText( entries, aBoard );
118 }
119
120 if( ruleText.IsEmpty() )
121 continue;
122
123 // Comments of deleted rules are kept, and a file rule split by an edit emits its own only once
124 int source = entries[0]->sourceRule;
125
126 if( source >= 0 && static_cast<size_t>( source ) >= nextTrivia
127 && static_cast<size_t>( source ) < aTrivia.leadingTrivia.size() )
128 {
129 while( nextTrivia <= static_cast<size_t>( source ) )
130 result += aTrivia.leadingTrivia[nextTrivia++];
131 }
132 else
133 {
134 result += wxS( "\n" );
135 }
136
137 result += ruleText;
138 }
139
140 while( nextTrivia < aTrivia.leadingTrivia.size() )
141 result += aTrivia.leadingTrivia[nextTrivia++];
142
143 if( aTrivia.header.IsEmpty() )
144 result += wxS( "\n" );
145 else
146 result += aTrivia.trailer;
147
148 return result;
149}
150
151
153 const BOARD* aBoard )
154{
155 // Round-trip preservation: return original text if not edited. A rule that was split
156 // into several entries must be regenerated once any of them is gone.
157 if( !aEntry.wasEdited && !aEntry.originalRuleText.IsEmpty() && aEntry.originalEntryCount == 1 )
158 return aEntry.originalRuleText;
159
160 // Otherwise, regenerate from panel data
161 if( !aEntry.constraintData )
162 return wxEmptyString;
163
164 wxString ruleText = aEntry.constraintData->GetGeneratedRule();
165
166 if( ruleText.IsEmpty() || aEntry.panelType == SILK_TO_SOLDERMASK_CLEARANCE
167 || aEntry.panelType == SILK_TO_SILK_CLEARANCE )
168 {
170 ctx.ruleName = aEntry.ruleName;
171 ctx.conditionExpression = aEntry.condition;
173 ctx.comment = aEntry.constraintData->GetComment();
174
176 {
177 wxString silkCond;
178
179 if( aEntry.layerCondition.test( F_SilkS ) && !aEntry.layerCondition.test( B_SilkS ) )
180 silkCond = wxS( "L == 'F.Mask'" );
181 else if( aEntry.layerCondition.test( B_SilkS ) && !aEntry.layerCondition.test( F_SilkS ) )
182 silkCond = wxS( "L == 'B.Mask'" );
183 else
184 silkCond = wxS( "L == 'F.Mask' || L == 'B.Mask'" );
185
186 if( !ctx.conditionExpression.IsEmpty() )
187 ctx.conditionExpression = wxS( "(" ) + silkCond + wxS( ") && " ) + ctx.conditionExpression;
188 else
189 ctx.conditionExpression = silkCond;
190 }
191 else if( aEntry.panelType == SILK_TO_SILK_CLEARANCE )
192 {
193 wxString silkCond;
194
195 if( aEntry.layerCondition.test( F_SilkS ) && !aEntry.layerCondition.test( B_SilkS ) )
196 silkCond = wxS( "L == 'F.SilkS'" );
197 else if( aEntry.layerCondition.test( B_SilkS ) && !aEntry.layerCondition.test( F_SilkS ) )
198 silkCond = wxS( "L == 'B.SilkS'" );
199 else
200 silkCond = wxS( "L == 'F.SilkS' || L == 'B.SilkS'" );
201
202 if( !ctx.conditionExpression.IsEmpty() )
203 ctx.conditionExpression = wxS( "(" ) + silkCond + wxS( ") && " ) + ctx.conditionExpression;
204 else
205 ctx.conditionExpression = silkCond;
206 }
207 else if( aBoard )
208 {
209 if( !aEntry.layerSource.IsEmpty() )
210 ctx.layerClause = formatLayerClause( aEntry.layerSource );
211 else
213 }
214
215 ruleText = aEntry.constraintData->GenerateRule( ctx );
216 }
217
218 // If severity is specified and not default, we need to inject it
219 // The GenerateRule method should handle this, but we verify here
221 {
222 wxString severityClause = generateSeverityClause( aEntry.severity );
223
224 if( !severityClause.IsEmpty() && !ruleText.Contains( "(severity" ) )
225 {
226 // Insert severity clause before the closing paren
227 size_t lastParen = ruleText.rfind( ')' );
228
229 if( lastParen != wxString::npos )
230 {
231 ruleText = ruleText.Left( lastParen ) + "\n\t" + severityClause + ")";
232 }
233 }
234 }
235
236 return ruleText;
237}
238
239
241{
242 // The parser gives a rule without a layer clause every layer
243 if( !aLayers.any() || aLayers == LSET::AllLayersMask() )
244 return wxEmptyString;
245
246 if( ( aLayers & LSET::AllCuMask() ) == LSET::ExternalCuMask() )
247 return wxString::Format( wxS( "(layer %s)" ), DRC_RULES_LEXER::TokenName( DRCRULE_T::T_outer ) );
248
249 if( ( aLayers & LSET::AllCuMask() ) == LSET::InternalCuMask() )
250 return wxString::Format( wxS( "(layer %s)" ), DRC_RULES_LEXER::TokenName( DRCRULE_T::T_inner ) );
251
252 // The parser only accepts a single layer name, and a user name only where the board registered
253 // it, so emit the canonical name of the first layer. Multi-layer sets use outer or inner above
254 for( PCB_LAYER_ID layer : aLayers.Seq() )
255 return wxString::Format( wxS( "(layer \"%s\")" ), LSET::Name( layer ) );
256
257 return wxEmptyString;
258}
259
260
262{
263 switch( aSeverity )
264 {
265 case RPT_SEVERITY_IGNORE: return "(severity ignore)";
266 case RPT_SEVERITY_WARNING: return "(severity warning)";
267 case RPT_SEVERITY_ERROR: return "(severity error)";
268 case RPT_SEVERITY_EXCLUSION: return "(severity exclusion)";
269 default: return wxEmptyString;
270 }
271}
272
273
275 const std::vector<const DRC_RE_LOADED_PANEL_ENTRY*>& aEntries,
276 const BOARD* aBoard )
277{
278 if( aEntries.empty() )
279 return wxEmptyString;
280
281 // Check if all entries are unedited and the first one has original text
282 // If so, we can use round-trip preservation
283 bool allUnedited = true;
284
285 for( const auto* entry : aEntries )
286 {
287 if( entry->wasEdited )
288 {
289 allUnedited = false;
290 break;
291 }
292 }
293
294 if( allUnedited && !aEntries[0]->originalRuleText.IsEmpty()
295 && static_cast<int>( aEntries.size() ) == aEntries[0]->originalEntryCount )
296 {
297 return aEntries[0]->originalRuleText;
298 }
299
300 // Otherwise, merge constraint clauses from all entries
301 const DRC_RE_LOADED_PANEL_ENTRY* firstEntry = aEntries[0];
302
304 ctx.ruleName = firstEntry->ruleName;
305 ctx.conditionExpression = firstEntry->condition;
306
307 // Generate layer clause from first entry with layer info (all entries in a
308 // merged group share the same layerSource because it's part of the grouping key)
309 for( const auto* entry : aEntries )
310 {
311 if( entry->layerCondition.any() && aBoard )
312 {
313 if( !entry->layerSource.IsEmpty() )
314 ctx.layerClause = formatLayerClause( entry->layerSource );
315 else
316 ctx.layerClause = generateLayerClause( entry->layerCondition );
317
318 break;
319 }
320 }
321
322 // Collect all constraint clauses from all entries
323 std::vector<wxString> allClauses;
324
325 for( const auto* entry : aEntries )
326 {
327 if( entry->constraintData )
328 {
330 entryCtx.ruleName = entry->ruleName;
331 entryCtx.conditionExpression = entry->condition;
332 entryCtx.constraintCode = entry->constraintData->GetConstraintCode();
333
334 auto clauses = entry->constraintData->GetConstraintClauses( entryCtx );
335
336 for( const wxString& clause : clauses )
337 {
338 if( !clause.IsEmpty() )
339 allClauses.push_back( clause );
340 }
341 }
342 }
343
344 // Build the merged rule
345 wxString rule;
346 rule << wxS( "(rule " ) << DRC_RE_BASE_CONSTRAINT_DATA::formatRuleName( ctx.ruleName )
347 << wxS( "\n" );
348
349 if( !ctx.layerClause.IsEmpty() )
350 rule << wxS( "\t" ) << ctx.layerClause << wxS( "\n" );
351
352 for( const wxString& clause : allClauses )
353 rule << wxS( "\t" ) << clause << wxS( "\n" );
354
355 if( !ctx.conditionExpression.IsEmpty() )
356 {
357 rule << wxS( "\t(condition \"" )
358 << EscapeString( ctx.conditionExpression, CTX_QUOTED_STR ) << wxS( "\")\n" );
359 }
360
361 // Add severity if any entry has non-default severity
362 for( const auto* entry : aEntries )
363 {
364 if( entry->severity != RPT_SEVERITY_UNDEFINED && entry->severity != RPT_SEVERITY_ERROR )
365 {
366 rule << wxS( "\t" ) << generateSeverityClause( entry->severity ) << wxS( "\n" );
367 break;
368 }
369 }
370
371 rule << wxS( ")" );
372
373 return rule;
374}
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:410
static wxString formatRuleName(const wxString &aRuleName)
Format a rule name for use in S-expression output while preserving the exact name the user chose.
virtual wxString GenerateRule(const RULE_GENERATION_CONTEXT &aContext)
wxString generateRuleText(const DRC_RE_LOADED_PANEL_ENTRY &aEntry, const BOARD *aBoard)
Generate the rule text for a single panel entry.
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.
bool SaveFile(const wxString &aPath, const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr, const DRC_RE_FILE_TRIVIA &aTrivia={})
Save all panel entries to a file.
wxString GenerateRulesText(const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr, const DRC_RE_FILE_TRIVIA &aTrivia={})
Generate rule text from panel entries.
wxString generateLayerClause(const LSET &aLayers)
Generate a layer clause from an LSET.
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:630
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition lset.cpp:309
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
static const LSET & AllLayersMask()
Definition lset.cpp:637
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:573
static wxString Name(PCB_LAYER_ID aLayerId)
Return the fixed name association with aLayerId.
Definition lset.cpp:184
wxString GetComment()
Get the comment associated with the rule.
@ SILK_TO_SILK_CLEARANCE
@ SILK_TO_SOLDERMASK_CLEARANCE
std::string source
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ F_SilkS
Definition layer_ids.h:96
@ B_SilkS
Definition layer_ids.h:97
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
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
The text of a rules file that lies outside every rule, kept so a save can reproduce it.
wxString header
Everything through the version expression, empty for a new file.
std::vector< wxString > leadingTrivia
Comments and blank lines before each rule, by sourceRule.
wxString trailer
Everything after the last rule.
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
int originalEntryCount
SEVERITY severity
wxString result
Test unit parsing edge cases and error handling.